Search Unity

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
     
    sirleto likes this.
  2. seandanger

    seandanger

    Joined:
    Jun 13, 2012
    Posts:
    39
    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:
    39
    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
     
    ZenTeapot likes this.
  5. JackHardyOnUnity

    JackHardyOnUnity

    Joined:
    Mar 5, 2018
    Posts:
    4
  6. bobbaluba

    bobbaluba

    Joined:
    Feb 27, 2013
    Posts:
    81
    Thanks both for the incredibly detailed analysis, this probably saved me several hours of debugging and tearing my hair out.

    Sadly unity has now marked this as "won't fix". So as far as I understand the only way to keep instancing and have per-instance data is to either cram it into the SpriteRenderer.color (vertex data) if you are not using it, or reimplement your own sprite renderer using MeshRenderer...

    I'm already using per-sprite colors and alpha, though. So I guess I'll have to go with the latter.
     
  7. seandanger

    seandanger

    Joined:
    Jun 13, 2012
    Posts:
    39
    That is disappointing they marked it as an "Edge case user workflow scenario". What other way is there to add instanced properties to a SpriteRenderer? That's a clever idea to pack your data into the color field -- too bad color is commonly used already though. If I were going to implement my own MeshRenderer I'd probably take a look at how something like 2DToolkit does it. Good luck!
     
  8. gwelkind

    gwelkind

    Joined:
    Sep 16, 2015
    Posts:
    66
    iirc I saw in another thread that Textures cannot be GPU instanced and this is a hardware limitation. It's a common misconception that GPU instancing and material property blocks must be used together. In fact, you can write a normal shader without any instancing logic and set them individually via material property blocks.

    iiuc, GPU instancing is a performance optimization to minimize draw calls and doesn't allow you to render anything you wouldn't be able to otherwise. It's only available for specific types (colors, floats, etc). Unity's documentation on this is poor, unfortunately.

    I'm able to get similar things working if I remove instancing logic from my shader and use Material Property blocks on their own.

    With this context, it makes a bit more sense that this is an "edge use case" scenario, since if you're trying to write a custom sprite renderer, you won't usually have a bottleneck which GPU instancing will help with. I think it's more for scenarios where you have 1000s of objects on screen which are nearly the same but have individually chosen colors which need to be determined on the main thread. Not usually something you need to do with sprites

    Hope that helps someone.
     
  9. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65

    I'm having exactly the same issue. MaterialPropertyBlock breaks instancing, even when all attributes set are exactly the same. Did you figure out a way to set per-instance data without using MPB? It's incredibly disappoint that Unity marked this as not fix. It definitely is not what Unity calls an "edge case user workflow scenario". That's incredibly lazy and dismissing from Unity.
     
    Last edited: Feb 3, 2023
  10. seandanger

    seandanger

    Joined:
    Jun 13, 2012
    Posts:
    39
    Unfortunately not. I accepted the draw call hit and moved on :(
     
    ryanflees likes this.
  11. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    I ended up packing per-vertex data into spriterenderer.color, ugly, unclean, but worked in my situation. So frustrated with this bug.
     
  12. ryanflees

    ryanflees

    Joined:
    Nov 15, 2014
    Posts:
    59
    I'm having a similiar issue here.
    I want to add instanced property to the sprite shader, so that some of the mobs in game can appear differently. But calling SetPropertyBlock will break the instancing on SpriteRenderers.
    I tested in unity 2022 and it doesn't work either.
    Just a feedback to everyone in the thread that in year 2023 Unity still doesn't have a solid solution for instanced sprites with custom properties.
    Now I'm planning to make a custom sprite renderer with quad mesh.
     
  13. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    Custom sprite renderer with mesh renderer is a rabbit hole in and of itself. I've been down that path. It could work, but not performant. The number of monbehavior updates on the CPU side will ruin performance if instance data is set every frame. You'll end up writing a lot of enable/disable code to manage this as well. Best of luck. It's just frustrating that the missing of a simple function drives people down these sort of twisted workarounds.
     
  14. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,514
    2023.1 beta has SRP Batcher working for Sprite Renderers now. Not the same thing as instancing but for many use cases it may be appropriate.
     
  15. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    Does it support setting custom per instance data without breaking batching?
     
  16. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,514
    SRP Batcher takes a different approach. More draw calls, but they're less expensive. It excels at letting you use different materials for the same shader inexpensively. So you can have custom per (material) instance data. It's not as performant as gpu instancing but it's more flexible and easy to use.

    https://blog.unity.com/technology/srp-batcher-speed-up-your-rendering
     
  17. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    Thanks for the link. I've read through it and smells like alll the other unity features, sound good initially, until you discover the hidden detaills. Probably give it a try in my next project. Too much to migrate with built-in pipeline at this point.
     
  18. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    I can think of at least one use case where you'd want proper sprite instancing and stuff like draw call batching wouldn't cover it. Imagine you're making a bullet hell/danmaku game with thousands of bullets on screen at once (and targeting Switch or something to avoid the inevitable hardware arguments). Eg....



    In this example most bullet sprites are the exact same, but you might want to do something in your shader for individual bullets besides just coloring them.

    Particles aren't an option if you need direct control over each bullet's trajectory (and if players are expected to make pixel-perfect dodges, the nondeterminism of particle systems would make balancing/tweaking really difficult).

    Of course, the "cowboy" answer is to toss all the bullet data into a compute buffer, run a compute shader to generate the vertices, and then render those. Which would probably work better than instancing. But that's quite complex, a PITA even if you know what you're doing, and dependent on the render pipeline and semi-documented Unity internals regarding render order.
     
    ZenTeapot likes this.
  19. ryanflees

    ryanflees

    Joined:
    Nov 15, 2014
    Posts:
    59
    I'm getting close to finish it.
    I follow the way in this blog (in Chinese)

    www.ownself.org/blog/2022/unity-sprite-gpu-instancing.html

    You could use a disabled SpriteRenderer along with your custom quad mesh.
    The key of doing this is:
    1, convert the SpriteRenderer's uv to quad's uv.
    2, Put the textures (should have same size) inside a Texture2DArray so that each renderer could find which texture it should use by index (which is an instanced property in the buffer).

    You could use a workflow that all textures in the project that shares same texture array, or use seperate arrays for each
    kind of SpriteRenderers, or more dedicated sorting by category (like characters ,mobs, items

    Calling set property block might have performance issue if they're called every frame for too many objects, so I only call it after detect any properties have been changed (like uv rect, flip, or other custom properties).

    I'm wrapping the SpriteRenderer with a component that can use origin SpriteRenderer or switch to GPU instancing quad mesh renderer.

    It's better to manually call Graphics's DrawInstance and Indirect APIs, and keep all the data in shader buffer and calculate them with compute shader. But that's too much task if for a small project and I don't need it right now.
    I'll give more feedback after my sprite component is done.

    "The number of monbehavior updates on the CPU side will ruin performance if instance data is set every frame"
    And this issue, for a better performance, you better don't use MonoBehaviors' default update. You could use a main controller of the game in scene to call all updates, all components will be traversed and called update (for example a public OnUpdate(float dt){}) manually, and it's also good for controlling the execute order as well.
     
  20. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    Thanks for the writeup. Yes I've done this before, pretty much touched all the points and with some even more twisted optmizations. It could work, just annoying as hell. I put the blame on unity for not allowing a simple customization of per-vertex data with sprite renderer.
     
  21. ryanflees

    ryanflees

    Joined:
    Nov 15, 2014
    Posts:
    59
    OK, I've been busy for a while, and add some followups.
    I use a quad mesh to draw instanced sprite, and set the material properties only if change is detected, as SetPropertyBlock is a little costly for too many calls each frame.
    It does give some improvement for a lot sprites to reduce the drawcall, however the improvement is very limited as the bottleneck of large amount of sprites is the physics, for example for a game like vampire survivors my framerate will drop increasingly after about 3~400 units. So there's no much need to optimize the drawcalls before I could optimize the physics. So I roll back to default SpriteRenderer which is sufficient for my need.
    However here's my abondoned instanced sprite , if the future people who googled into this post of the forum, this probably would help a little bit.
    https://drive.google.com/file/d/1Pn5t1F0EG54M4Wt7p5tVTPVzb2hL8lEF/view?usp=share_link

    A simple way I find is that I could enable ECS in my project and it would automatically optimize the default physics engine. I could have up to 1000 units with 60 fps, so if I set the maximum count of units to like 5~600 with default SpriteRenderer, it's okay already.

    I don't need more units, but if for more units, it probably must be done in ECS,with manually call Graphics.DrawMeshInstanced() or Graphics.DrawMeshInstancedIndirect().
    There are some reference for massive collisions and renderering.
    https://github.com/unitycoder/Unity-DOTS-RTS-Collision-System
    https://github.com/paullj/unity-ecs-instanced-sprite-renderer
     
    Last edited: Mar 17, 2023