Search Unity

Change order in which passes are drawn?

Discussion in 'Shaders' started by Sinterklaas, Jan 19, 2021.

  1. Sinterklaas

    Sinterklaas

    Joined:
    Jun 6, 2018
    Posts:
    93
    I've written a shader that allows for fancy lighting in a sprite-based game, but I've ran into a problem that I'm unsure how to fix.

    The shader has three passes: one ForwardBase pass for opaque pixels (which writes to the depth buffer), one ForwardBase pass for transparent pixels (which does not write depth), and lastly the ForwardAdd pass. The reason that my ForwardBase pass is split in two is because depth writing is useful for certain effects that I want to achieve. It's a weird setup, but just roll with it.

    Everything is mostly working as I want, with one exception: when two sprites on the same sorting layer intersect, and they have the same order value on that layer, there will be strange clipping issues.

    Now, this isn't a huge deal, but it means that I have to be very careful with setting up the rendering order of every drawn sprite in my game, so that no two sprites with the same order and layer can overlap. Which sounds like a big welcoming sign for visual glitches to me!

    upload_2021-1-19_13-58-5.png
    This is what everything should look like with one white directional light and one red point light (the sprites have different order values in this picture).

    upload_2021-1-19_13-58-27.png
    And this is what happens if the order values are identical.

    As you can see, the pixels drawn by the transparent and ForwardAdd passes overlap with the second sprite.
    I'm guessing this is because the pass drawing order goes: sprite 1 opaque -> sprite 2 opaque -> sprite 1 transparent -> sprite 2 transparent -> sprite 1 ForwardAdd -> sprite2 ForwardAdd.
    Instead of: sprite 1 opaque -> sprite 1 transparent -> sprite 1 ForwardAdd -> sprite 2 opaque -> sprite 2 transparent -> sprite 2 ForwardAdd, like I had originally assumed.

    Is this line of reasoning correct? And if so, how can I alter the rendering order? Do I need to write a custom renderer to achieve this, or is there an easier way? Should I even want to change the render order, or will this destroy performance?

    By the by, here's my shader code. I've left out the .cginc file that contains the actual vertex and fragment program, because it's very long. If it's helpful I can post that too.

    Code (CSharp):
    1. Shader "Sprite3D/Sprite3D" {
    2.  
    3.     Properties{
    4.         [NoScaleOffset] _MainTex("RGBA Texture", 2D) = "white" {}
    5.         [NoScaleOffset] _OutlineTex("Outline Texture", 2D) = "black" {}
    6.         [NoScaleOffset] _NormalTex("Normal Texture", 2D) = "bump" {}
    7.         [NoScaleOffset] _SpecularTex("Specular Texture", 2D) = "white" {}
    8.         [NoScaleOffset] _GlossTex("Gloss Texture", 2D) = "white" {}
    9.         [NoScaleOffset] _LightTex("Light Texture", 2D) = "white" {}
    10.         [NoScaleOffset] _OcclusionTex("Occlusion Texture", 2D) = "white" {}
    11.         [NoScaleOffset] _EmissionTex("Emission Texture", 2D) = "white" {}
    12.         _ColorTint("Color Tint", Color) = (1, 1, 1, 1)
    13.         _AlphaCutoff("Alpha Cutoff", Range(0, 1)) = 1
    14.         _SpecularTint("Specular Tint", Color) = (0.5, 0.5, 0.5, 1)
    15.         _Glossiness("Glossiness", Range(0.01, 1)) = 0.5
    16.         _Radiance("Radiance", Range(0, 1)) = 1
    17.         _Occlusion("Occlusion", Range(0, 1)) = 1
    18.         _Emission("Emission", Range(0, 1)) = 0
    19.     }
    20.  
    21.     CGINCLUDE
    22.     #define NORMAL_MAP_ENABLED
    23.     #define SPECULAR_MAP_ENABLED
    24.     #define GLOSS_MAP_ENABLED
    25.     #define LIGHT_MAP_ENABLED
    26.     #define OCCLUSION_MAP_ENABLED
    27.     #define EMISSION_MAP_ENABLED
    28.     #define BINORMAL_PER_FRAGMENT
    29.     #define SPECULAR_LIGHTING_ENABLED
    30.     ENDCG
    31.  
    32.     SubShader
    33.     {
    34.         // OPAQUE PASS
    35.         Pass
    36.         {
    37.             Tags
    38.             {
    39.                 "LightMode" = "ForwardBase"
    40.             }
    41.  
    42.             Blend Off
    43.             ZTest LEqual
    44.             ZWrite On
    45.  
    46.             CGPROGRAM
    47.  
    48.             #pragma target 3.0
    49.  
    50.             #pragma multi_compile _ SHADOWS_SCREEN
    51.             #pragma multi_compile _ VERTEXLIGHT_ON
    52.  
    53.             #pragma vertex MyVertexProgram
    54.             #pragma fragment MyFragmentProgram
    55.  
    56.             #define OPAQUE_PASS
    57.  
    58.             #include "Sprite3DLighting.cginc"
    59.  
    60.             ENDCG
    61.         }
    62.          
    63.         // TRANSPARENT PASS
    64.         Pass
    65.         {
    66.             Tags
    67.             {
    68.                 "LightMode" = "ForwardBase"
    69.             }
    70.  
    71.             Blend SrcAlpha OneMinusSrcAlpha
    72.             ZTest LEqual
    73.             ZWrite Off
    74.  
    75.             CGPROGRAM
    76.  
    77.             #pragma target 3.0
    78.  
    79.             #pragma multi_compile _ SHADOWS_SCREEN
    80.             #pragma multi_compile _ VERTEXLIGHT_ON
    81.  
    82.             #pragma vertex MyVertexProgram
    83.             #pragma fragment MyFragmentProgram
    84.  
    85.             #define TRANSPARENT_PASS
    86.  
    87.             #include "Sprite3DLighting.cginc"
    88.  
    89.             ENDCG
    90.         }
    91.          
    92.         // FORWARD-ADD PASS
    93.         Pass
    94.         {
    95.             Tags
    96.             {
    97.                 "LightMode" = "ForwardAdd"
    98.             }
    99.  
    100.             Blend One One
    101.             ZTest LEqual
    102.             ZWrite Off
    103.  
    104.             CGPROGRAM
    105.  
    106.             #pragma target 3.0
    107.  
    108.             #pragma multi_compile_fwdadd_fullshadows
    109.  
    110.             #pragma vertex MyVertexProgram
    111.             #pragma fragment MyFragmentProgram
    112.  
    113.             #define FORWARD_ADD_PASS
    114.  
    115.             #include "Sprite3DLighting.cginc"
    116.  
    117.             ENDCG
    118.         }
    119.     }
    120. }
    Edit: Nevermind, there's a scrollview. Here's the bloody thing. Apologies for the messiness.
    Code (CSharp):
    1. #if !defined(MY_LIGHTING_INCLUDED)
    2. #define MY_LIGHTING_INCLUDED
    3.  
    4. #include "UnityPBSLighting.cginc"
    5. #include "AutoLight.cginc"
    6.  
    7. // UNIFORMS
    8. sampler2D _MainTex;
    9. float4 _MainTex_ST;
    10. sampler2D _OutlineTex;
    11. #if defined(NORMAL_MAP_ENABLED)
    12. sampler2D _NormalTex;
    13. #endif
    14. #if defined(SPEC_MAP_ENABLED)
    15. sampler2D _SpecularTex;
    16. #endif
    17. #if !defined(GLO_TEXTURE_PACKING_ENABLED)
    18. #if defined(GLOSS_MAP_ENABLED)
    19. sampler2D _GlossTex;
    20. #endif
    21. #if defined (LIGHT_MAP_ENABLED)
    22. sampler2D _LightTex;
    23. #endif
    24. #if defined(OCCLUSION_MAP_ENABLED)
    25. sampler2D _OcclusionTex;
    26. #endif
    27. #else
    28. sampler2D _GLOTex;
    29. #endif
    30. #if defined(EMISSION_MAP_ENABLED)
    31. sampler2D _EmissionTex;
    32. #endif
    33.  
    34. float4 _ColorTint;
    35. float _AlphaCutoff;
    36. float4 _SpecularTint;
    37. float _Glossiness;
    38. float _Radiance;
    39. float _Occlusion;
    40. float _Emission;
    41.  
    42. // TYPES
    43. struct VertexData {
    44.     float4 vertex : POSITION;
    45.     float4 color : COLOR;
    46.     float3 normal : NORMAL;
    47.     float4 tangent : TANGENT;
    48.     float2 uv : TEXCOORD0;
    49. };
    50.  
    51. struct Interpolators {
    52.     float4 pos :    SV_POSITION;
    53.     float4 color :    TEXCOORD5;
    54.     float4 uv :        TEXCOORD0;
    55.     float3 normal :    TEXCOORD1;
    56.  
    57.     #if defined(BINORMAL_PER_FRAGMENT)
    58.         float4 tangent : TEXCOORD2;
    59.     #else
    60.         float3 tangent : TEXCOORD2;
    61.         float3 binormal : TEXCOORD3;
    62.     #endif
    63.  
    64.     float3 worldPos : TEXCOORD4;
    65.  
    66.     SHADOW_COORDS(5)
    67. };
    68.  
    69. // VERTEX
    70. float3 CreateBinormal (float3 normal, float3 tangent, float binormalSign) {
    71.     return cross(normal, tangent.xyz) *
    72.         (binormalSign * unity_WorldTransformParams.w);
    73. }
    74.  
    75. Interpolators MyVertexProgram (VertexData v) {
    76.     Interpolators i;
    77.     i.pos = UnityObjectToClipPos(v.vertex);
    78.     i.worldPos = mul(unity_ObjectToWorld, v.vertex);
    79.     i.color = v.color;
    80.     i.normal = UnityObjectToWorldNormal(v.normal);
    81.  
    82.     #if defined(NORMAL_MAP_ENABLED)
    83.     #if defined(BINORMAL_PER_FRAGMENT)
    84.         i.tangent = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
    85.     #else
    86.         i.tangent = UnityObjectToWorldDir(v.tangent.xyz);
    87.         i.binormal = CreateBinormal(i.normal, i.tangent, v.tangent.w);
    88.     #endif
    89.     #endif
    90.    
    91.     i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
    92.  
    93.     TRANSFER_SHADOW(i);
    94.     return i;
    95. }
    96.  
    97. // FRAGMENT
    98. // Lighting functions below. Don't ask me how these work, for I haven't got a clue.
    99. UnityLight CreateLight (Interpolators i, float radiance) {
    100.     UnityLight light;
    101.  
    102.     #if defined(POINT) || defined(POINT_COOKIE) || defined(SPOT)
    103.         light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    104.     #else
    105.         light.dir = _WorldSpaceLightPos0.xyz;
    106.     #endif
    107.  
    108.     UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
    109.  
    110.     light.color = _LightColor0.rgb * attenuation * radiance;
    111.     light.ndotl = DotClamped(i.normal, light.dir);
    112.     return light;
    113. }
    114.  
    115. float3 BoxProjection (
    116.     float3 direction, float3 position,
    117.     float4 cubemapPosition, float3 boxMin, float3 boxMax
    118. ) {
    119.     #if UNITY_SPECCUBE_BOX_PROJECTION
    120.         UNITY_BRANCH
    121.         if (cubemapPosition.w > 0) {
    122.             float3 factors =
    123.                 ((direction > 0 ? boxMax : boxMin) - position) / direction;
    124.             float scalar = min(min(factors.x, factors.y), factors.z);
    125.             direction = direction * scalar + (position - cubemapPosition);
    126.         }
    127.     #endif
    128.     return direction;
    129. }
    130.  
    131. UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir, float occlusion) {
    132.     UnityIndirect indirectLight;
    133.     indirectLight.diffuse = 0;
    134.     indirectLight.specular = 0;
    135.  
    136.     #if defined(OPAQUE_PASS) || defined(TRANSPARENT_PASS)
    137.         indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
    138.         float3 reflectionDir = reflect(-viewDir, i.normal);
    139.         Unity_GlossyEnvironmentData envData;
    140.         envData.roughness = 1 - _Glossiness;
    141.         envData.reflUVW = BoxProjection(
    142.             reflectionDir, i.worldPos,
    143.             unity_SpecCube0_ProbePosition,
    144.             unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
    145.         );
    146.         float3 probe0 = Unity_GlossyEnvironment(
    147.             UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
    148.         );
    149.         envData.reflUVW = BoxProjection(
    150.             reflectionDir, i.worldPos,
    151.             unity_SpecCube1_ProbePosition,
    152.             unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax
    153.         );
    154.         #if UNITY_SPECCUBE_BLENDING
    155.             float interpolator = unity_SpecCube0_BoxMin.w;
    156.             UNITY_BRANCH
    157.             if (interpolator < 0.99999) {
    158.                 float3 probe1 = Unity_GlossyEnvironment(
    159.                     UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),
    160.                     unity_SpecCube0_HDR, envData
    161.                 );
    162.                 indirectLight.specular = lerp(probe1, probe0, interpolator);
    163.             }
    164.             else {
    165.                 indirectLight.specular = probe0;
    166.             }
    167.         #else
    168.             indirectLight.specular = probe0;
    169.         #endif
    170.     #endif
    171.  
    172.     indirectLight.diffuse *= occlusion;
    173.     indirectLight.specular *= occlusion;
    174.  
    175.     return indirectLight;
    176. }
    177.  
    178.  
    179. // Functions below that read our various texture maps, if they have been enabled.
    180. float3 CalcFragNormal(inout Interpolators i) {
    181.     #if defined(NORMAL_MAP_ENABLED)
    182.     float3 normal = UnpackScaleNormal(tex2D(_NormalTex, i.uv.xy), 1);
    183.  
    184.     #if defined(BINORMAL_PER_FRAGMENT)
    185.         float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w);
    186.     #else
    187.         float3 binormal = i.binormal;
    188.     #endif
    189.    
    190.     i.normal = normalize(i.normal);
    191.     return normalize(normal.x * i.tangent + normal.y * binormal + normal.z * i.normal);
    192.     #else
    193.     return normalize(i.normal);
    194.     #endif
    195. }
    196.  
    197. float3 CalcSpecular(Interpolators i)
    198. {
    199.     #if defined(SPECULAR_LIGHTING_ENABLED)
    200.         #if defined(SPEC_MAP_ENABLED)
    201.         return _SpecularTint.rgb * _SpecularTint.a * tex2D(_SpecularTex, i.uv.xy);
    202.         #else
    203.         return _SpecularTint.rgb * _SpecularTint.a;
    204.         #endif
    205.     #else
    206.     return 0;
    207.     #endif
    208. }
    209.  
    210. float CalcGloss(Interpolators i)
    211. {
    212.     #if defined(GLOSS_MAP_ENABLED)
    213.     return _Glossiness * tex2D(_GlossTex, i.uv.xy);
    214.     #else
    215.     return _Glossiness;
    216.     #endif
    217. }
    218.  
    219. float CalcRadiance(Interpolators i)
    220. {
    221.     #if defined(LIGHT_MAP_ENABLED)
    222.     return _Radiance * tex2D(_LightTex, i.uv.xy);
    223.     #else
    224.     return _Radiance;
    225.     #endif
    226. }
    227.  
    228. float CalcOcclusion(Interpolators i)
    229. {
    230.     #if defined(OCCLUSION_MAP_ENABLED)
    231.     return _Occlusion * tex2D(_OcclusionTex, i.uv.xy);
    232.     #else
    233.     return _Occlusion;
    234.     #endif
    235. }
    236.  
    237. float4 CalcEmission(Interpolators i)
    238. {
    239.     #if defined(EMISSION_MAP_ENABLED)
    240.     return _Emission * tex2D(_EmissionTex, i.uv.xy);
    241.     #else
    242.     return _Emission;
    243.     #endif
    244. }
    245.  
    246. // Main frag program.
    247. float4 MyFragmentProgram(Interpolators i) : SV_TARGET{
    248.  
    249.     // Debug color for undefined passes.
    250.     #if !defined (OPAQUE_PASS) && !defined(TRANSPARENT_PASS) && !defined(FORWARD_ADD_PASS)
    251.     return float4(0, 1, 1, 1);
    252.  
    253.     // Legal passes.
    254.     #else
    255.  
    256.     // Calculate view direction.
    257.     float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
    258.  
    259.     // Read outline texture.
    260.     float4 outline = tex2D(_OutlineTex, i.uv.xy);
    261.  
    262.     // Read color texture, figure out albedo.
    263.     float4 rgba = tex2D(_MainTex, i.uv.xy);
    264.     float3 albedo = rgba.rgb * _ColorTint.rgb * i.color.rgb;
    265.  
    266.     // Figure out alpha. Discard pixel if alpha has a certain value, depending on the pass.
    267.     float alpha = min(outline.a + rgba.a, 1) * _ColorTint.a * i.color.a;
    268.    
    269.     #if defined(OPAQUE_PASS)
    270.     if (alpha < _AlphaCutoff - 0.001)
    271.         discard;
    272.     #elif defined(TRANSPARENT_PASS)
    273.     if (alpha == 0 || alpha >= _AlphaCutoff)
    274.         discard;
    275.     #elif defined(FORWARD_ADD_PASS)
    276.     if (alpha == 0)
    277.         discard;
    278.     #endif
    279.  
    280.     // Figure out normal, specular intensity, gloss, radiance, occlusion and emission. Gloss, radiance and occlusion are read from one
    281.     // texture if channel packing is enabled.
    282.     float3 normal = CalcFragNormal(i);
    283.     float3 specular = CalcSpecular(i);
    284.     #if !defined(GLO_TEXTURE_PACKING_ENABLED)
    285.     float glossiness = CalcGloss(i);
    286.     float radiance = CalcRadiance(i);
    287.     float occlusion = CalcOcclusion(i);
    288.     #else
    289.     float3 glo = tex2D(_GLOTex, i.uv.xy);
    290.     float glossiness = _Glossiness * glo.r;
    291.     float radiance = _Radiance * glo.g;
    292.     float occlusion = _Occlusion * glo.b;
    293.     #endif
    294.     float4 emission = CalcEmission(i);
    295.  
    296.     // Run Unity PBS lighting.
    297.     float oneMinusReflectivity;
    298.     EnergyConservationBetweenDiffuseAndSpecular(albedo, specular, /*out*/ oneMinusReflectivity);
    299.  
    300.     float3 lit = UNITY_BRDF_PBS(
    301.         albedo, specular,
    302.         oneMinusReflectivity, glossiness,
    303.         normal, viewDir,
    304.         CreateLight(i, radiance), CreateIndirectLight(i, viewDir, occlusion)
    305.     );
    306.  
    307.     // Add emission. In the ForwardAdd pass, we simply reduce the effect of light on emissive pixels.
    308.     #if defined(OPAQUE_PASS) || defined(TRANSPARENT_PASS)
    309.     float3 emissioned = emission.rgb * emission.a + lit.rgb * (1 - emission.a);
    310.     #elif defined (FORWARD_ADD_PASS)
    311.     float3 emissioned = (1 - emission.a) * lit;
    312.     #endif
    313.  
    314.     // Add outline.
    315.     float3 outlined = emissioned * (1 - outline.a) + outline.rgb;
    316.  
    317.     // Add alpha.
    318.     #if defined(OPAQUE_PASS) || defined(TRANSPARENT_PASS)
    319.     float4 alphad = float4(outlined, alpha);
    320.     #elif defined (FORWARD_ADD_PASS)
    321.     float4 alphad = float4(outlined, 1) * alpha;
    322.     #endif
    323.  
    324.     // Return final color.
    325.     return alphad;
    326.     #endif
    327. }
    328.  
    329. #endif
     

    Attached Files:

  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Try adding this to the
    SubShader
    .
    Code (csharp):
    1. Tags { "DisableBatching" = "True" }
    Again, this should go at the start of the
    SubShader
    , not in a
    Pass
    .
     
  3. Sinterklaas

    Sinterklaas

    Joined:
    Jun 6, 2018
    Posts:
    93
    I added this to the subshader tags, but to my surprise it didn't really do anything? I was sure it had to be a problem with batching.

    Anyway, I realized that I could also write an editor script to automatically take care of SpriteRenderer ordering, so I did. In the unlikely event that anyone else has the same issue, and stumbles upon this thread, here's the mostly untested but seemingly working script.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.SceneManagement;
    5. using UnityEditor;
    6.  
    7. [CustomEditor(typeof(SpriteRenderer))]
    8. public class SpriteRendererEditor : Editor
    9. {
    10.     private static bool foldout;
    11.  
    12.     private static List<List<SpriteRenderer>> sortingOrderPerLayer;
    13.     private static string[] sortingLayerNames;
    14.     private static double lastSortingUpdateTime;
    15.     private static int sceneID;
    16.  
    17.     public override void OnInspectorGUI()
    18.     {
    19.         // Get a shorthand to the target SpriteRenderer.
    20.         SpriteRenderer t = (SpriteRenderer)target;
    21.  
    22.         // Get a bunch of spacing information.
    23.         float inspectorWidth = EditorGUIUtility.currentViewWidth - 12f;
    24.         float labelX = 18f;
    25.         float labelWidth = EditorGUIUtility.labelWidth;
    26.         float fieldX = labelX + labelWidth + 2f;
    27.         float fieldWidth = inspectorWidth - fieldX - 4f;
    28.         float lineHeight = EditorGUIUtility.singleLineHeight;
    29.         float verticalSpacing = lineHeight + EditorGUIUtility.standardVerticalSpacing;
    30.         Rect rect = new Rect(labelX, 4f, labelWidth, lineHeight);
    31.  
    32.         // Figure out sorting order per sorting layer. Also stores an array of sorting layer names.
    33.         UpdateSorting();
    34.  
    35.         // Record changes for undo purposes.
    36.         Undo.RecordObject(t, t.name + " Changes");
    37.  
    38.  
    39.         // Draw inspector fields.
    40.  
    41.         // Sprite.
    42.         EditorGUI.LabelField(rect, "Sprite");
    43.  
    44.         rect.x = fieldX;
    45.         rect.width = fieldWidth;
    46.         t.sprite = (Sprite)EditorGUI.ObjectField(rect, t.sprite, typeof(Sprite), false);
    47.  
    48.         // Color.
    49.         rect.x = labelX;
    50.         rect.y += verticalSpacing;
    51.         rect.width = labelWidth;
    52.         EditorGUI.LabelField(rect, "Color");
    53.  
    54.         rect.x = fieldX;
    55.         rect.width = fieldWidth;
    56.         t.color = EditorGUI.ColorField(rect, t.color);
    57.  
    58.         // Flip.
    59.         rect.x = labelX;
    60.         rect.y += verticalSpacing;
    61.         rect.width = labelWidth;
    62.         EditorGUI.LabelField(rect, "Flip");
    63.  
    64.         // Flip X.
    65.         rect.x = fieldX;
    66.         rect.width = fieldWidth / 2f - 2f;
    67.         t.flipX = EditorGUI.Toggle(rect, t.flipX);
    68.  
    69.         rect.x += 20f;
    70.         rect.width -= 20f;
    71.         EditorGUI.LabelField(rect, "X");
    72.  
    73.         // Flip Y.
    74.         rect.x = fieldX + fieldWidth / 2f + 2f;
    75.         rect.width = fieldWidth / 2f - 2f;
    76.         t.flipY = EditorGUI.Toggle(rect, t.flipY);
    77.  
    78.         rect.x += 20f;
    79.         rect.width -= 20f;
    80.         EditorGUI.LabelField(rect, "Y");
    81.  
    82.         // Draw mode.
    83.         rect.x = labelX;
    84.         rect.y += verticalSpacing;
    85.         rect.width = labelWidth;
    86.         EditorGUI.LabelField(rect, "Draw Mode");
    87.  
    88.         rect.x = fieldX;
    89.         rect.width = fieldWidth;
    90.         t.drawMode = (SpriteDrawMode)EditorGUI.EnumPopup(rect, t.drawMode);
    91.  
    92.         // Mask interaction.
    93.         rect.x = labelX;
    94.         rect.y += verticalSpacing;
    95.         rect.width = labelWidth;
    96.         EditorGUI.LabelField(rect, "Mask Interaction");
    97.  
    98.         rect.x = fieldX;
    99.         rect.width = fieldWidth;
    100.         t.maskInteraction = (SpriteMaskInteraction)EditorGUI.EnumPopup(rect, t.maskInteraction);
    101.  
    102.         // Material.
    103.         rect.x = labelX;
    104.         rect.y += verticalSpacing;
    105.         rect.width = labelWidth;
    106.         EditorGUI.LabelField(rect, "Material");
    107.  
    108.         rect.x = fieldX;
    109.         rect.width = fieldWidth;
    110.         t.sharedMaterial = (Material)EditorGUI.ObjectField(rect, t.sharedMaterial, typeof(Material), false);
    111.  
    112.         // Additional settings.
    113.         rect.x = labelX;
    114.         rect.y += verticalSpacing;
    115.         rect.width = labelWidth;
    116.         foldout = EditorGUI.Foldout(rect, foldout, "Additional Settings");
    117.  
    118.         if (foldout)
    119.         {
    120.             // Sorting layer.
    121.             int sortingLayerIndex = GetSortingLayerIndex(t.sortingLayerName);
    122.  
    123.             rect.x = labelX + 10f;
    124.             rect.y += verticalSpacing;
    125.             EditorGUI.LabelField(rect, "Sorting Layer");
    126.  
    127.             rect.x = fieldX;
    128.             rect.width = fieldWidth;
    129.             int targetSortingLayerIndex = EditorGUI.Popup(rect, sortingLayerIndex, sortingLayerNames);
    130.  
    131.             if (sortingLayerIndex != targetSortingLayerIndex)
    132.                 MoveToLayer(sortingLayerIndex, targetSortingLayerIndex, -t.sortingOrder);
    133.  
    134.             // Sorting order.
    135.             rect.x = labelX + 10f;
    136.             rect.y += verticalSpacing;
    137.             EditorGUI.LabelField(rect, "Sorting Order");
    138.  
    139.             rect.x = fieldX;
    140.             rect.width = fieldWidth;
    141.             EditorGUI.LabelField(rect, t.sortingOrder.ToString());
    142.  
    143.             rect.x = labelX;
    144.             rect.y += verticalSpacing;
    145.             rect.width = (inspectorWidth - labelX - 4f) / 4f - 4f;
    146.             if (GUI.Button(rect, "Backwards"))
    147.                 MoveBack(t);
    148.  
    149.             rect.x += rect.width + 4f;
    150.             if (GUI.Button(rect, "Forwards"))
    151.                 MoveForward(t);
    152.  
    153.             rect.x += rect.width + 4f;
    154.             if (GUI.Button(rect, "To Back"))
    155.                 MoveToBack(t);
    156.  
    157.             rect.x += rect.width + 4f;
    158.             if (GUI.Button(rect, "To Front"))
    159.                 MoveToFront(t);
    160.         }
    161.  
    162.  
    163.         // Set height of inspector.
    164.         GUILayoutUtility.GetRect(inspectorWidth, rect.y + rect.height - 4f);
    165.     }
    166.  
    167.     /// <summary>
    168.     /// Get the 2D list of SpriteRenderers in the scene, sorted by sorting layer and sorting order (which enforces contiguous, unique sorting
    169.     /// order values within a sorting layer, where 0 is the highest value within on each layer). Also stores the names of the project's
    170.     /// sorting layers.
    171.     /// </summary>
    172.     private void UpdateSorting()
    173.     {
    174.         // We only want to run this function once per editor update.
    175.         if (EditorApplication.timeSinceStartup == lastSortingUpdateTime && EditorApplication.timeSinceStartup != 0d)
    176.             return;
    177.  
    178.         // Get all SpriteRenderers in the scene.
    179.         SpriteRenderer[] renderers = FindObjectsOfType<SpriteRenderer>();
    180.  
    181.         // Get all sorting layer names.
    182.         int prevLayerNum = sortingLayerNames != null ? sortingLayerNames.Length : 0;
    183.         SortingLayer[] sortingLayers = SortingLayer.layers;
    184.         sortingLayerNames = new string[sortingLayers.Length];
    185.         for (int i = 0; i < sortingLayers.Length; i++)
    186.         {
    187.             sortingLayerNames[i] = sortingLayers[i].name;
    188.         }
    189.  
    190.         // Completely rebuild our sorting order list if the number of SpriteRenderers in the scene has changed, the number of sorting layers
    191.         // has changed, or we opened a different scene.
    192.         if (sortingOrderPerLayer == null || sortingOrderPerLayer.Count != renderers.Length || prevLayerNum != sortingLayerNames.Length
    193.             || sceneID != SceneManager.GetActiveScene().buildIndex)
    194.         {
    195.             // Create new double list.
    196.             sortingOrderPerLayer = new List<List<SpriteRenderer>>();
    197.  
    198.             for (int i = 0; i < sortingLayerNames.Length; i++)
    199.             {
    200.                 // Add one list per sorting layer.
    201.                 sortingOrderPerLayer.Add(new List<SpriteRenderer>());
    202.  
    203.                 // Add all renderers of this sorting layer.
    204.                 for (int j = 0; j < renderers.Length; j++)
    205.                 {
    206.                     if (renderers[j].sortingLayerName == sortingLayerNames[i])
    207.                     {
    208.                         // Figure out if we need to insert the renderer in the list.
    209.                         bool placed = false;
    210.                         for (int k = 0; k < sortingOrderPerLayer[i].Count; k++)
    211.                         {
    212.                             if (sortingOrderPerLayer[i][k].sortingOrder <= renderers[j].sortingOrder)
    213.                             {
    214.                                 sortingOrderPerLayer[i].Insert(k, renderers[j]);
    215.                                 placed = true;
    216.                                 break;
    217.                             }
    218.                         }
    219.  
    220.                         // If we haven't inserted it, put it at the end.
    221.                         if (!placed)
    222.                             sortingOrderPerLayer[i].Add(renderers[j]);
    223.                     }
    224.                 }
    225.             }
    226.         }
    227.  
    228.         // Ensure that every SpriteRenderer's sorting order is equal to their list index, but made negative.
    229.         for (int i = 0; i < sortingOrderPerLayer.Count; i++)
    230.         {
    231.             for (int j = 0; j < sortingOrderPerLayer[i].Count; j++)
    232.             {
    233.                 sortingOrderPerLayer[i][j].sortingOrder = -j;
    234.             }
    235.         }
    236.  
    237.         // Store current editor time as our new last sorting update timestamp.
    238.         lastSortingUpdateTime = EditorApplication.timeSinceStartup;
    239.     }
    240.  
    241.     /// <summary>
    242.     /// Get the array index corresponding to a sorting layer.
    243.     /// </summary>
    244.     private int GetSortingLayerIndex(string sortingLayerName)
    245.     {
    246.         int index = -1;
    247.         for (int i = 0; i < sortingLayerNames.Length; i++)
    248.         {
    249.             if (sortingLayerNames[i] == sortingLayerName)
    250.             {
    251.                 index = i;
    252.                 break;
    253.             }
    254.         }
    255.         return index;
    256.     }
    257.  
    258.     /// <summary>
    259.     /// Move a SpriteRenderer to another sorting layer. This changes the sorting order value, and puts it at the back of that layer.
    260.     /// </summary>
    261.     private void MoveToLayer(int srcLayerIndex, int dstLayerIndex, int sortingOrderIndex)
    262.     {
    263.         // Remove SpriteRenderer from src layer's list.
    264.         SpriteRenderer renderer = sortingOrderPerLayer[srcLayerIndex][sortingOrderIndex];
    265.         sortingOrderPerLayer[srcLayerIndex].RemoveAt(sortingOrderIndex);
    266.  
    267.         // Move the SpriteRenderer to the back of the dst layer's list.
    268.         renderer.sortingOrder = -sortingOrderPerLayer[dstLayerIndex].Count;
    269.         renderer.sortingLayerName = sortingLayerNames[dstLayerIndex];
    270.         sortingOrderPerLayer[dstLayerIndex].Add(renderer);
    271.  
    272.         // Ensure that every SpriteRenderer's sorting order in the src list is equal to their list index, but made negative.
    273.         for (int i = 0; i < sortingOrderPerLayer[srcLayerIndex].Count; i++)
    274.         {
    275.             sortingOrderPerLayer[srcLayerIndex][i].sortingOrder = -i;
    276.         }
    277.     }
    278.  
    279.     /// <summary>
    280.     /// Move a SpriteRenderer back on its sorting layer, by one spot. Does nothing if we are already at the back (sorting order value is the
    281.     /// lowest negative number on that layer).
    282.     /// </summary>
    283.     private void MoveBack(SpriteRenderer t)
    284.     {
    285.         // Get the indices within the 2D sorting list.
    286.         int sortingLayerIndex = GetSortingLayerIndex(t.sortingLayerName);
    287.         int sortingOrderIndex = Mathf.Abs(t.sortingOrder);
    288.  
    289.         // If we aren't the backmost renderer already, swap with next SpriteRenderer.
    290.         if (sortingOrderIndex != sortingOrderPerLayer[sortingLayerIndex].Count - 1)
    291.             SwapSortingOrders(sortingLayerIndex, sortingOrderIndex, sortingOrderIndex + 1);
    292.     }
    293.  
    294.     /// <summary>
    295.     /// Move a SpriteRenderer forward on its sorting layer, by one spot. Does nothing if it's already at the front (sorting order value equals
    296.     /// 0).
    297.     /// </summary>
    298.     private void MoveForward(SpriteRenderer t)
    299.     {
    300.         // Get the indices within the 2D sorting list.
    301.         int sortingLayerIndex = GetSortingLayerIndex(t.sortingLayerName);
    302.         int sortingOrderIndex = Mathf.Abs(t.sortingOrder);
    303.  
    304.         // If we aren't the front renderer already, swap with previous SpriteRenderer.
    305.         if (sortingOrderIndex != 0)
    306.             SwapSortingOrders(sortingLayerIndex, sortingOrderIndex, sortingOrderIndex - 1);
    307.     }
    308.  
    309.     /// <summary>
    310.     /// Move a SpriteRenderer to the back of its sorting layer (giving it the lowest negative sorting order value on that layer).
    311.     /// </summary>
    312.     private void MoveToBack(SpriteRenderer t)
    313.     {
    314.         // Get the indices within the 2D sorting list.
    315.         int sortingLayerIndex = GetSortingLayerIndex(t.sortingLayerName);
    316.         int sortingOrderIndex = -t.sortingOrder;
    317.  
    318.         // While we aren't the backmost renderer, keep swapping with the previous SpriteRenderer.
    319.         while (sortingOrderIndex != sortingOrderPerLayer[sortingLayerIndex].Count - 1)
    320.         {
    321.             SwapSortingOrders(sortingLayerIndex, sortingOrderIndex, sortingOrderIndex + 1);
    322.             sortingOrderIndex++;
    323.         }
    324.     }
    325.  
    326.     /// <summary>
    327.     /// Move a SpriteRenderer to the front of its sorting layer (giving it a sorting order value of 0).
    328.     /// </summary>
    329.     private void MoveToFront(SpriteRenderer t)
    330.     {
    331.         // Get the indices within the 2D sorting list.
    332.         int sortingLayerIndex = GetSortingLayerIndex(t.sortingLayerName);
    333.         int sortingOrderIndex = -t.sortingOrder;
    334.  
    335.         // While we aren't the front renderer, keep swapping with the previous SpriteRenderer.
    336.         while (sortingOrderIndex != 0)
    337.         {
    338.             SwapSortingOrders(sortingLayerIndex, sortingOrderIndex, sortingOrderIndex - 1);
    339.             sortingOrderIndex--;
    340.         }
    341.     }
    342.  
    343.     /// <summary>
    344.     /// Swap two items within one sorting layer.
    345.     /// </summary>
    346.     private void SwapSortingOrders(int sortingLayer, int src, int dst)
    347.     {
    348.         // Swap sorting order values of the SpriteRenderers.
    349.         int tempOrder = sortingOrderPerLayer[sortingLayer][src].sortingOrder;
    350.         sortingOrderPerLayer[sortingLayer][src].sortingOrder = sortingOrderPerLayer[sortingLayer][dst].sortingOrder;
    351.         sortingOrderPerLayer[sortingLayer][dst].sortingOrder = tempOrder;
    352.  
    353.         // Swap array entries.
    354.         SpriteRenderer tempRenderer = sortingOrderPerLayer[sortingLayer][src];
    355.         sortingOrderPerLayer[sortingLayer][src] = sortingOrderPerLayer[sortingLayer][dst];
    356.         sortingOrderPerLayer[sortingLayer][dst] = tempRenderer;
    357.     }
    358. }
    359.  
    Though I'm still pretty curious what the issue was, consider this question answered, I guess.

    Edit: Added some additional commentary and fixed a minor bug.
     
    Last edited: Jan 27, 2021
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I was worried it might not solve the problem. It would have about 4 years ago. ;)

    Without your script and without the "DisableBatching" line, if you use the Frame Debugger window you'll see that when you have two sprites each shader pass is rendered as a single draw. This is because Unity batches sprites using the same material & sprite (or sprite atlas) together by default. This means both sprites are combined into a single mesh, and since it's a single mesh each shader pass is rendered one after another for the whole mesh leading to what you got above.

    Using "DisableBatching" prevents Unity from combining the sprites, and if you look at the frame debugger you'll see each sprite is rendered individually again. However about 4 years ago Unity made a change to how it sorted draws. Now if there are multiple objects that are using the same material, it'll render all of the objects with the first pass, then all of the objects with the second pass, etc. So while what's actually happening has changed when using "DisableBatching", the end result for you has not.

    For 3D opaque objects, this is a non issue as the depth buffer will fix any sorting issues caused by rendering the passes in different orders, as long as the first pass is using
    ZWrite On
    and all the subsequent passes use exactly the same vertex positions. But for transparent or co-planar objects this is a problem.

    Your script appears to be working around the issue with the sorting order. I honestly would not have expected that to of fixed the problem, as the sorting order is usually just used to explicitly control the sprite order, but I thought still allowed them to batch together. I wonder, are you using the above script with "DisableBatching" in your shader still? And does removing "DisableBatching" make it stop working if it is in the shader? I don't really use sprites so it's an area of Unity I'm not super familiar with.


    Other solutions that might be slightly less painful to use:
    • Add a sorting group and a "hidden" child sprite to each character. Needs to be a sprite that's not using the same material as your character sprite, but is enabled. I think you can get away with it being scaled to zero on one axis, or you may need to create a 1 pixel sprite that's entirely invisible and set to use "full rect" so Unity doesn't collapse the sprite mesh to nothing. That'll inject a draw between each character that should prevent it from trying to do any kind of batching or sort the draws by shader pass.
    • Create a custom material for each sprite. Doesn't actually have to change any value, just be a copy of the original material. That'll also prevent it from batching, and should prevent it from sorting the draws by shader pass. I don't know how "smart" the pass sorting is though, so this might not actually work.
     
    Last edited: Jan 21, 2021
  5. Sinterklaas

    Sinterklaas

    Joined:
    Jun 6, 2018
    Posts:
    93
    Ah, that clears things up! I'm not super familiar with the frame debugger, in fact I didn't even know there was a frame debugger to begin with, so that's certainly a useful tip for the future!

    I had removed the DisableBatching tag. And, if I'm understanding the frame debugger correctly, adding it back in doesn't seem to change anything either, if the editor script is used. Before, the blending problem would only show up if SpriteRenderers had equal sorting layers and sorting order values (and also the same z position).

    So I'm guessing that sprite sorting probably interferes with batching, though I'm not familiar enough with Unity's rendering pipeline to say for certain, this being my first real attempt at shader writing.

    Either way, thank you for your help!