Search Unity

  1. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice
  2. Ever participated in one our Game Jams? Want pointers on your project? Our Evangelists will be available on Friday to give feedback. Come share your games with us!
    Dismiss Notice

Adding an Instanced Property breaks Instancing when used with Sprite Renderer properties

Discussion in 'Shaders' started by JackHardyOnUnity, Dec 15, 2019.

  1. JackHardyOnUnity

    JackHardyOnUnity

    Joined:
    Mar 5, 2018
    Posts:
    4
    I'm attempting to add a new instanced property to a shader that's used to render sprites. The Sprite Renderer is able to set two instanced properties, _RendererColor and _Flip, which I'm able to make use of in my shader and which work correctly with instancing. I now need to add my own instanced properties but doing so breaks instancing and I'm unsure why.

    I'm making use of a MaterialPropertyBlock to set an additional Color property called _InstancedColor (as I know _RendererColor works so the type is supported), the Property is declared in my .shader, and in the .hlsl it's declared with UNITY_DEFINE_INSTANCED_PROP(float4, _InstancedColor) within a UNITY_INSTANCING_BUFFER_START/END and accessed with UNITY_ACCESS_INSTANCED_PROP. I'll post the full shader and script code below with some screens of the frame debugger to show what is happening.

    The main difference between my property and the ones coming from the SpriteRenderer is that the SpriteRenderer property names don't match in the Instancing buffer and the Property block. In the Property block _RendererColor is used, but within the Instancing buffer they declare unity_SpriteRendererColorArray and then use #define _RendererColor UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, unity_SpriteRendererColorArray) to allow the use of the Property name. I've attempted to do something similar but it still caused the same issues and I've found no explanation in the documentation as to why this would need to be done.

    Also if I clear the MaterialPropertyBlock that comes from the SpriteRenderer and then just set my instanced Color value the instancing does seem to work correctly but as soon as I try and set _MainTex to a texture the instancing breaks again.

    The .shader file
    Code (Shader):
    1. Shader "My Pipeline/L2DLSpriteStandard"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite", 2D) = "white" {}
    6.         _Color ("Tint", Color) = (1,1,1,1)
    7.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    8.         [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
    9.         [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
    10.         [HideInInspector] _InstancedColor ("Instanced Color", Color) = (1,1,1,1)
    11.     }
    12.  
    13.     SubShader
    14.     {
    15.         Pass
    16.         {
    17.             Name "L2DLSpriteStandard"
    18.  
    19.             Tags
    20.             {
    21.                 "Queue" = "Transparent"
    22.                 "RenderType"="Transparent"
    23.                 "PreviewType"="Plane"
    24.             }
    25.  
    26.             ZWrite Off
    27.             Blend One OneMinusSrcAlpha
    28.  
    29.             HLSLPROGRAM
    30.  
    31.             #pragma target 3.5
    32.  
    33.             #pragma multi_compile_instancing
    34.  
    35.             #pragma vertex SpriteVert
    36.             #pragma fragment SpriteFrag
    37.  
    38.             #include "../ShaderLibrary/L2DLSpriteStandard.hlsl"
    39.  
    40.             ENDHLSL
    41.         }
    42.     }
    43. }
    The .hlsl file
    Code (hlsl):
    1. #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    2.  
    3. CBUFFER_START(UnityPerFrame)
    4.     float4x4 unity_MatrixVP;
    5. CBUFFER_END
    6.  
    7. CBUFFER_START(UnityPerDraw)
    8.     float4x4 unity_ObjectToWorld;
    9. CBUFFER_END
    10.  
    11. // Instancing support
    12. #define UNITY_MATRIX_M unity_ObjectToWorld
    13.  
    14. #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
    15.  
    16. UNITY_INSTANCING_BUFFER_START(PerDrawSprite)
    17.     UNITY_DEFINE_INSTANCED_PROP(float4, unity_SpriteRendererColorArray)
    18.     UNITY_DEFINE_INSTANCED_PROP(float2, unity_SpriteFlipArray)
    19.     UNITY_DEFINE_INSTANCED_PROP(float4, _InstancedColor)
    20. UNITY_INSTANCING_BUFFER_END(PerDrawSprite)
    21.  
    22. #define _RendererColor  UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, unity_SpriteRendererColorArray)
    23. #define _Flip           UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, unity_SpriteFlipArray)
    24.  
    25. // Material Color.
    26. float4 _Color;
    27.  
    28. struct appdata_t
    29. {
    30.     float4 vertex   : POSITION;
    31.     float4 color    : COLOR;
    32.     float2 texcoord : TEXCOORD0;
    33.     UNITY_VERTEX_INPUT_INSTANCE_ID
    34. };
    35.  
    36. struct v2f
    37. {
    38.     float4 vertex   : SV_POSITION;
    39.     float4 color    : COLOR;
    40.     float2 texcoord : TEXCOORD0;
    41. };
    42.  
    43. inline float4 UnityFlipSprite(in float3 pos, in float2 flip)
    44. {
    45.     return float4(pos.xy * flip, pos.z, 1.0);
    46. }
    47.  
    48. inline float4 UnityObjectToClipPos(in float4 vert)
    49. {
    50.     return mul(unity_MatrixVP, mul(UNITY_MATRIX_M, float4(vert.xyz, 1.0)));
    51. }
    52.  
    53. v2f SpriteVert(appdata_t IN)
    54. {
    55.     v2f OUT;
    56.  
    57.     UNITY_SETUP_INSTANCE_ID (IN);
    58.  
    59.     OUT.vertex = UnityFlipSprite(IN.vertex, _Flip);
    60.     OUT.vertex = UnityObjectToClipPos(OUT.vertex);
    61.     OUT.texcoord = IN.texcoord;
    62.     OUT.color = IN.color * _Color * _RendererColor * UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, _InstancedColor);
    63.  
    64.     #ifdef PIXELSNAP_ON
    65.     OUT.vertex = UnityPixelSnap (OUT.vertex);
    66.     #endif
    67.  
    68.     return OUT;
    69. }
    70.  
    71. TEXTURE2D(_MainTex);
    72. SAMPLER(sampler_MainTex);
    73.  
    74. float4 SpriteFrag(v2f IN) : SV_Target0
    75. {
    76.     float4 spriteColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.texcoord);
    77.     spriteColor.rgb *= spriteColor.a;
    78.     return spriteColor * IN.color;
    79. }
    The script on the object
    Code (CSharp):
    1. [RequireComponent(typeof(SpriteRenderer))]
    2. public class InstancedSprite : MonoBehaviour
    3. {
    4.     private static MaterialPropertyBlock s_matPropBlock;
    5.     private static int s_colorPropertyID = Shader.PropertyToID("_InstancedColor");
    6.  
    7.     [SerializeField] private Color m_color = Color.white;
    8.    
    9.     private void Awake()
    10.     {
    11.         SetColorToMesh();
    12.     }
    13.  
    14.     private void OnValidate()
    15.     {
    16.         SetColorToMesh();
    17.     }
    18.  
    19.     private void SetColorToMesh()
    20.     {
    21.         if(s_matPropBlock == null)
    22.         {
    23.             s_matPropBlock = new MaterialPropertyBlock();
    24.         }
    25.  
    26.         GetComponent<Renderer>().GetPropertyBlock(s_matPropBlock);
    27.         s_matPropBlock.SetColor(s_colorPropertyID, m_color);
    28.         GetComponent<Renderer>().SetPropertyBlock(s_matPropBlock);
    29.     }
    30. }
    When the script is not attached instancing works fine using the SpriteRenderer's color value, which is set to unity_SpriteRendererColorArray behind the scenes.
    upload_2019-12-15_10-0-25.png

    When I attach the script and change the _InstancedColor value the instancing breaks due to 'Non-instanced properties set for instanced shader."
    upload_2019-12-15_10-6-23.png

    When I attach the script but don't Get the MaterialPropertyBlock first (removing the properties set by the SpriteRenderer) instancing works correctly and I can change the colors using my new _InstancedColor property (but I then loose the texture and renderer color information).
    upload_2019-12-15_10-4-24.png

    Any help is obviously appreciated, it's pretty likely there's a macro or something that I've missed but I'm running out of things to try.

    Jack
     
  2. seandanger

    seandanger

    Joined:
    Jun 13, 2012
    Posts:
    36
    I read this incredibly well detailed and written post with bated breath hoping there'd be a solution down here below it, because I'm having the same issue with nearly the same setup. My only differences are I'm using cg instead of HLSL and I'm trying to add a float instanced property instead of a float4. But I also based my code on Unity's default sprite shader code.

    My shader is intended to show a drink's fill amount by using an empty sprite and a full sprite, and simply using the _FillAmount property to determine which to sample from. When I set the MaterialPropertyBlock using a script identical to yours, I get no instancing and the Frame Debugger says 'Non-instanced properties set for instanced shader." as well. I really wish it would print out the names of the properties in question here!



    Like you, if I omit the GetPropertyBlock line from my component script, then instancing works and everything is batched into 1 Batch and 1 SetPass call, but the objects all simply take the property that was set on the last instance in the Hierarchy.



    I'm fairly stumped as for what to do next as well. As far as I can tell, I've followed the manual to the letter.
     
  3. seandanger

    seandanger

    Joined:
    Jun 13, 2012
    Posts:
    36
    I think I figured it out!

    2 things, the first didn't matter in the end but I'm including it here just to show that it was considered as well.

    Unlike the default sprite shader code, the manual uses
    UNITY_INSTANCING_CBUFFER_START
    , (note: CBUFFER, not BUFFER). This macro expands thusly in my code:
    Code (CSharp):
    1. UNITY_INSTANCING_BUFFER_START(MyProps)
    2.     UNITY_DEFINE_INSTANCED_PROP(float, _FillAmount)
    3. #define _FillAmount_arr MyProps // added by macro
    4. UNITY_INSTANCING_BUFFER_END(MyProps)
    But, if I remove the #define _FillAmount_arr MyProps line, nothing changes and my shader still works / instances properly. I think the default shader code you and I referenced is using an older syntax for Instancing in general.

    On to the important bit:

    Thanks to this great tutorial, I noticed that I was trying to access the instanced property in my fragment function, but I hadn't called any setup macro or function on it. So I added a call to
    UNITY_TRANSFER_INSTANCE_ID
    in my vertex function, and then a
    UNITY_SETUP_INSTANCE_ID
    call to the top of my fragment function. That got the shader working, but I would sometimes see an error in the console during runtime complaining about _FillAmount being undefined in the shader. I ended up moving
    UNITY_DEFINE_INSTANCED_PROP(float, _FillAmount)
    out of the PerDrawSprite properties block and making my own new one, as I theorized that that PerDrawSprite code from the default shader may be outdated or using special keywords, or something else. I don't fully understand it. Anyway, moving my property to its own new block seems to have solved the issue.

    Another thing I noticed is that the catlikecoding tutorial I linked doesn't bother with all the #ifdef and #ifndef handling of instanced properties. It explains that the macros like
    UNITY_ACCESS_INSTANCED_PROP
    handle that themselves, so I didn't bother with it on my new _FillAmount property, but I did leave the existing properties as-is.

    Here is the full shader for reference.

    As for your code @JackHardyOnUnity, I'm not sure what the precise fix is, because unlike me, you weren't accessing your instanced property in the fragment program. I wonder if moving your property to its own
    UNITY_INSTANCING_BUFFER_START
    block would work?

    --Edit--
    Just realized that instancing only works when I omit the GetPropertyBlock call in my component script. Now I'm starting to think that the properties we've included from the default sprite shader code is what is actually breaking the instancing, not our added properties.
     
    Last edited: Feb 13, 2020
  4. JackHardyOnUnity

    JackHardyOnUnity

    Joined:
    Mar 5, 2018
    Posts:
    4
    Hey @seandanger, it's nice to know someone else is having this problem!

    I totally gave up in frustration eventually so I never really got any further, and I stopped checking this forum so apologies for taking so long to follow up. I've just given your suggestions a go to no effect unfortunately, though it looks like from your -Edit- they may not have worked for you either?

    I spent a bit of time re-acquainting myself with the issue as well, testing how far I can go until the instancing breaks, and I agree that it's something to do with the Sprite Renderer's Material Property Block manipulation. Without ever getting this Block I can manage, as you also did, to set multiple values and instance them correctly. I can even use
    spriteRenderer.sharedMaterial.SetTexture("_MainTex", spriteRenderer.sprite.texture);

    to kinda fake having access to the SpriteRenderer's texture, though as you say all objects using that material will end up using the texture of the last thing in the hierarchy.
    Setting this texture to _MainTex via the MaterialPropertyBlock.SetTexture will break instancing though, even if all objects are using the same texture, due to the ever-helpful 'Non-instanced properties set for instanced shader.' I swear though that the SpriteRenderer is setting _MainTex using the MaterialPropertyBlock as once I stop Getting it I lose the sprite texture data.

    I think the next step could be to try and work out the state of the MaterialPropertyBlock when it arrives from the SpriteRenderer, but it all seems to be hidden away in ways I don't understand how to access (DecompiledVersion). Other than the once-again-extremely-helpful 'isEmpty' property you get no information about what is stored within.

    That being said at this point I think I might report it as a bug in the hopes that someone at Unity will have a look and either confirm that it's a bug , or explain why it's a feature and how to work with it. I think we've tried following enough official documentation and tutorials that we can say from a user's perspective this is not the intended behavior.

    Thanks for giving me some hope, I'll spend some more time this week seeing if I can understand it better and I'll add back here if I make any progress, or if the bug gets a response.

    Jack
     
  5. JackHardyOnUnity

    JackHardyOnUnity

    Joined:
    Mar 5, 2018
    Posts:
    4
unityunity