• RuntimesUnity
  • Batching with per material settings

Please let us know which Spine skeleton component you are using (I assume SkeletonGraphic from the context), and also which shader.

Which per-renderer settings differ in your case? If it's the main texture, then no, that would require the textures of your multiple skeletons (e.g. at least one atlas texture per different skeleton) to be pre-packed to another even larger atlas texture. A per-renderer texture typically only works with small (sprite) images which are packed to a single atlas by Unity, which and are then batched.

In general, normal Unity limitations apply regarding batching, which differ by used render pipeline (SRP batcher behaves differently). The Spine shaders should not break anything in regards to normal batching behaviour, however Unity's requirements might likely prevent what you would like to have batched.

    Related Discussions
    ...

    Harald
    We are using SkeletonAnimation(SkeletonMesh + MeshRenderer).
    I've made a custom shader to control some parameters since our characters waddle through water.

    	Properties{
    		_Color ("Tint Color", Color) = (1,1,1,1)
    		_Black ("Dark Color", Color) = (0,0,0,0)
    		[NoScaleOffset] _MainTex("Main Texture", 2D) = "black" {}
    		_Cutoff("Shadow alpha cutoff", Range(0,1)) = 0.1
    		_WaterSurface("Water surface", Float) = 0.1
    		_MaskHeight("Mask height", Float) = 0.1
    		_MaskWidth("Mask width", Float) = 0.5
    	}
    
    		SubShader{
    			Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" }
    
    			Fog { Mode Off }
    			Cull Off
    			ZWrite Off
    			Blend One OneMinusSrcAlpha
    			Lighting Off
    
    			Pass {
    				CGPROGRAM
    				#pragma vertex vert
    				#pragma fragment frag
    				#include "UnityCG.cginc" 
    				sampler2D _MainTex;
    				float4 _Color;
    				float4 _Black;
    				float _WaterSurface;
    				float _MaskHeight;
    				float _MaskWidth;
    				fixed _CutOff;
    
    				struct VertexInput {
    					float4 vertex : POSITION;
    					float2 uv : TEXCOORD0;
    					float4 vertexColor : COLOR;
    				};
    
    				struct VertexOutput {
    					float4 pos : SV_POSITION;
    					float2 uv : TEXCOORD0;
    					float3 worldPos : TEXCOORD1;
    					float4 vertexColor : COLOR;
    				};
    
    				VertexOutput vert(VertexInput v) {
    					VertexOutput o;
    					o.pos = UnityObjectToClipPos(v.vertex);
    					o.uv = v.uv;
    					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    					o.vertexColor = v.vertexColor * float4(_Color.rgb * _Color.a, _Color.a); // Combine a PMA version of _Color with vertexColor.
    					return o;
    				}
    
    #ifndef SKELETON_TINT_COMMON_INCLUDED
    #define SKELETON_TINT_COMMON_INCLUDED
    				float4 fragTintedColor(float4 texColor, float3 darkTintColor, float4 lightTintColorPMA, float lightColorAlpha, float darkColorAlpha) {
    
    					float a = texColor.a * lightTintColorPMA.a;
    
    				#if !defined(_STRAIGHT_ALPHA_INPUT)
    					float3 texDarkColor = (texColor.a - texColor.rgb);
    				#else
    					float3 texDarkColor = (1 - texColor.rgb);
    				#endif
    					float3 darkColor = texDarkColor * darkTintColor.rgb * lightColorAlpha;
    					float3 lightColor = texColor.rgb * lightTintColorPMA.rgb;
    
    					float4 fragColor = float4(darkColor + lightColor, a);
    				#if defined(_STRAIGHT_ALPHA_INPUT)
    					fragColor.rgb *= texColor.a;
    				#endif
    
    				#if defined(_DARK_COLOR_ALPHA_ADDITIVE)
    					fragColor.a = a * (1 - darkColorAlpha);
    				#endif
    					return fragColor;
    				}
    #endif
    
    				float4 frag(VertexOutput i) : COLOR {
    					float4 texColor = tex2D(_MainTex, i.uv);
    
    					const float PI = 3.1415926535897932384626433832795;
    					fixed height = _MaskHeight * sin(((i.worldPos.x - _MaskWidth * 0.5)/_MaskWidth) * PI);
    					texColor = fragTintedColor(texColor, _Black.rgb, i.vertexColor, _Color.a, _Black.a);
    					texColor = lerp( float4(0.0,0.0,0.0,0.0), texColor, step( _WaterSurface, i.worldPos.y - height ));
    					return texColor;
    				}
    				ENDCG
    			}
    		}
    }

    Basically some of these parameters vary between characters depending on where they are located.
    Is there anyway to change these parameters and have batching work?

    Thanks for the additional info. I forgot to explicitly ask: which render pipeline are you using?

    If you are using URP, you should in general likely wrap your shader variable declarations in

    CBUFFER_START(UnityPerMaterial)
     ..
    CBUFFER_END

    (or similar) sections (see the official Unity documentation here, and this nice thirdparty writeup here). If you are using MaterialPropertyBlocks, note that these break batching with SRP batcher (as documented here).

    What you can always do is pass down information as vertex data manually (additional vertex data, as e.g. normals and tangents are only added if needed). You would then either add specific parameter values as vertex data at each vertex, which you then read in your custom shader. If you have a lot of varying parameters per skeleton or material, you would then pass down an index to a constant-buffer parameter array instead (like instancing uses a single instance-id and then uses e.g. objectToWorldMatrix[instanceID] to transform each instance). The latter requires quite a lot of work however when compared to automatic batching, so you would likely want to get normal automatic batching working first.

      5 місяців пізніше

      Harald Thank you for the detailed info.

      I haven't had time to implement this yet. Getting back into it now.
      As for the render pipeline, it's neither? Not sure, our quality settings, do not contain any render pipeline asset(Unity 2020.3) not sure what Unity defaults to.
      As for the solutions you've mentioned, I am afraid I would need more guidance.
      Currently I am using MaterialPropertyBlocks for two cases with this shader:

      • To tint upon impact(Color animated over time)
      • To "cut-off" based on water depth(float set each frame based on skeleton position)

      If I could get the batching working with those two requirements in mind it would help our performance quite a bit. Could you give me some suggestions as to how I can pass data down?
      Also since I've never worked with URP, is that something we should consider? We are heavily sprite/spine based and are focus on Android/iOS.

      Thanks in advance and sorry for a rather long silence on my end.

        WiseKodama Sorry for the late reply, Harald is currently on vacation and will be back next Monday, so please wait until then he will come back and confirm your reply.

        4 дні пізніше
        • Змінено

        Sorry for the late reply as well, back from my vacation.

        WiseKodama As for the render pipeline, it's neither? Not sure, our quality settings, do not contain any render pipeline asset(Unity 2020.3) not sure what Unity defaults to.

        Thanks for the info. If no RenderPipelineAsset is assigned in the Graphics or Quality settings, then you're using the Standard Render Pipeline (not URP, Universal Render Pipeline, which is a scriptable render pipeline).

        Also since I've never worked with URP, is that something we should consider? We are heavily sprite/spine based and are focus on Android/iOS.

        Standard Render Pipeline should be fine, as URP might have some advantaged but also comes with its own set of problems. You could of course perform some measurements on the target device with the same scene (and comparable shaders) using different render pipelines, that would give the definitive answer.

        WiseKodama As for the solutions you've mentioned, I am afraid I would need more guidance.

        Do you mean you need more information about passing data per vertex?
        If so, then please see this forum posting below about passing additional vertex data to your shader:
        https://esotericsoftware.com/forum/d/18476-how-to-add-additional-uvs-to-skeletongraphic/3
        You would then need to read the vertex data in the shader accordingly (read it in the vertex shader and pass it to the fragment shader, then use it there) and use it instead of your material parameter.

        Currently I am using MaterialPropertyBlocks for two cases with this shader:
        To tint upon impact(Color animated over time)
        To "cut-off" based on water depth(float set each frame based on skeleton position)

        If I could get the batching working with those two requirements in mind it would help our performance quite a bit

        As always, please make sure to perform some measurements on your target device. Successive draw calls using the same Material with only different MaterialPropertyBlock parameters might be cheap enough to not matter. 10 draw calls using the same material and MaterialPropertyBlocks should be far cheaper than using 10 different materials with entirely different shaders.

        To be sure that the modifications are worth it you could compare performance with the identical material settings vs. modified tint and water height values. If you can't measure any difference, then you won't get any benefit from your modifications either.

        WiseKodama Currently I am using MaterialPropertyBlocks for two cases with this shader:
        To tint upon impact(Color animated over time)
        To "cut-off" based on water depth(float set each frame based on skeleton position)

        If you would only require tint upon impact, you could get away without any (or with fewer) code modification. That's because the spine-unity runtime comes with tint-black functionality. You can enable Advanced - Tint Black at the SkeletonAnimation component which then passes a second color (the dark color) as vertex attribute to the used shader. Note that the "dark color" property is set per Slot however (not per Skeleton).

        Nevertheless, if apart from tinting upon impact you also need a water level parameter, I'm afraid you would need to write some code to pass this information to the shader.

        An even easier solution might be the following: If you don't use the primary vertex color (which tints by multiplying your texture color with it) at all and always have it set to white, you could get by without any additional vertex data at all: you then could use the primary vertex color to convey all information that you need in your shader: you could for example pack the water level into the alpha channel of the color (normalized to reasonable values, as they will end up as values 0-255), and pass the tint color in the RGB channel. Then you can set skeletonAnimation.Skeleton.SetColor(Color yourPackedColor); to have the information passed to your shader.

        Thanks @Harald and welcome back, hope it was a relaxing vacation with plenty of good weather.
        I will have a go at this this week and report back, cheers.

        • Harald вподобали це.
        11 днів пізніше

        @Harald I've checked this now and it seems that since we are using 3.8 I cannot utilise the uv2 and uv3 vertex data, meaning that we would have to update to 4.2, where those are added, correct?
        If I see correctly 4.2 is currently still in beta. If we were to update to 4.2, would you consider it safe and are there any breaking changes? Also, would everything need to be reexported with 4.2?

        @WiseKodama As always, all Spine skeletons would need to be reexported when changing the spine-unity runtime from 3.8 (or 4.0 or 4.1) to 4.2-beta.

        If you're using spine-unity 3.8, you could simply integrate the changes of this commit from the 4.2-beta branch into your local spine-unity 3.8 installation:
        EsotericSoftware/spine-runtimesa8e6552
        Since we will not release updates to the 3.8 runtimes anymore, you don't risk to override your changes when installing a runtime patch update.

        @Harald That worked perfectly, implemented and in testing phase.
        Now if I could only somehow batch more than 300 vertices...

        Thanks so much for the help, cheers.

        @WiseKodama Glad it helped, thanks for getting back to us!

        Now if I could only somehow batch more than 300 vertices...

        It's a limitation based on the size of the vertex attributes, so if you could get rid of some vertex attributes (color?), it should increase the vertex limit. That would require changes to the MeshGenerator class however, and could involve quite some coding to make sure it has no undesired side-effects.

          8 місяців пізніше

          Greetings Harald,

          Sorry for reviving this old thread.
          We have recently updated to Unity 2022 and I noticed that there is now a sprite batching threshold setting in Unity.
          Does that play well together with Spine, meaning that increasing the threshold we can use it to go past this limit?

          Harald It's a limitation based on the size of the vertex attributes, so if you could get rid of some vertex attributes (color?), it should increase the vertex limit. That would require changes to the MeshGenerator class however, and could involve quite some coding to make sure it has no undesired side-effects.

          @WiseKodama If you mean PlayerSettings.spriteBatchVertexThreshold, the Unity documentation page reads:

          Unity will automatically batch SpriteRenderers into the same draw call if they share the same Material and fulfill other criteria.

          However spine-unity does not use a SpriteRenderer, it uses MeshRenderer and CanvasRenderer, so I'm not sure this setting will affect anything. To be honest though, I haven't tried it yet. so if you experience anything different and it's indeed affecting the MeshRenderers of SkeletonAnimation as well, please do let us know.