unity如何在urp管线下合并spine的渲染批次

对于导入unity的spine来说,他会对每个spine生成独有的材质,虽然他们使用的是同一个shader,但由于附带独有的贴图,这样在项目使用中会由于材质贴图不同而导致无法合批.

而为什么选用urp,因为在built-in管线中,对于GPU-instancing,即使通过使用图集的方式统一了贴图,也会由于spine组件在渲染时所生成的网格随着动画播放导致即使是同一个spine,每一帧的网格也会不一样,最后由于网格不同而无法通过GPU-instancing合批.

而使用texture数组的方式在GPU-instancing更不可行,因为贴图无法作为GPU-instancing的合法属性,因此只能转换管线为urp考虑使用Dynamic batching或是SRP batching.

在urp管线下使用Dynamic batching条件很简单,首先在配置文件开启

现在开启了动态合批,我们要想办法让所有的spine使用同一个贴图,不然他们会单独绘制,如下图所示:

这样是不行的,必须让他们使用同一张贴图,这里解决思路是把用到的贴图打包成图集,当运行时让shader去根据设定的uv采样对应区域以获得原本的贴图.

这里用一个脚本计算对应的贴图在shader中的位置,一共使用了三个变量去确定采样,下方为计算思路。

对于已知要采样的uv1点(u1,v1)  可以计算出这个点相对于整个texture左下角的位置(x1,y1)  [需传入整张texture的宽高]  
根据这个已知要采样的uv1点(u1,v1)  可以计算出换算后的点相对于精灵矩形左下角的位置(x2,y2)  [需传入精灵texture的宽高] 
再加上精灵矩形左下角的坐标,计算出换算点的位置(x3,y3)  [需传入精灵texture左下角相对于整个texture的位置坐标]
根据换算点的位置(x3,y3)  可以计算出换算点在整个texture的uv

 可知要传入图集的宽高,图集中对应精灵的宽高,以及这张精灵的左下角相对于图集左下角的位置,即可把要采样的uv映射到既定的精灵范围内。而这里是改造的spine-shader:“CustomURP/SpineTestShader”,它是基于“Spine/Skeleton Fill”改造,移除了不需要的功能,这里贴出来Spine/Skeleton Fill。

// - Unlit
// - Premultiplied Alpha Blending (Optional straight alpha input)
// - Double-sided, no depth

Shader "Spine/Skeleton Fill" {
	Properties {
		_FillColor ("FillColor", Color) = (1,1,1,1)
		_FillPhase ("FillPhase", Range(0, 1)) = 0
		[NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
		_Cutoff ("Shadow alpha cutoff", Range(0,1)) = 0.1
		[Toggle(_STRAIGHT_ALPHA_INPUT)] _StraightAlphaInput("Straight Alpha Texture", Int) = 0
		[HideInInspector] _StencilRef("Stencil Reference", Float) = 1.0
        [HideInInspector][Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comparison", Float) = 8 // Set to Always as default

        // Outline properties are drawn via custom editor.
		[HideInInspector] _OutlineWidth("Outline Width", Range(0,8)) = 3.0
		[HideInInspector] _OutlineColor("Outline Color", Color) = (1,1,0,1)
		[HideInInspector] _OutlineReferenceTexWidth("Reference Texture Width", Int) = 1024
		[HideInInspector] _ThresholdEnd("Outline Threshold", Range(0,1)) = 0.25
		[HideInInspector] _OutlineSmoothness("Outline Smoothness", Range(0,1)) = 1.0
		[HideInInspector][MaterialToggle(_USE8NEIGHBOURHOOD_ON)] _Use8Neighbourhood("Sample 8 Neighbours", Float) = 1
		[HideInInspector] _OutlineOpaqueAlpha("Opaque Alpha", Range(0,1)) = 1.0
		[HideInInspector] _OutlineMipLevel("Outline Mip Level", Range(0,3)) = 0
	}
	SubShader {
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
		Blend One OneMinusSrcAlpha
		Cull Off
		ZWrite Off
		Lighting Off

		Stencil {
			Ref[_StencilRef]
			Comp[_StencilComp]
			Pass Keep
		}

		Pass {
			Name "Normal"

			CGPROGRAM
			#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			#include "CGIncludes/Spine-Common.cginc"
			sampler2D _MainTex;
			float4 _FillColor;
			float _FillPhase;

			struct VertexInput {
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
			};

			struct VertexOutput {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
			};

			VertexOutput vert (VertexInput v) {
				VertexOutput o = (VertexOutput)0;
				o.uv = v.uv;
				o.vertexColor = PMAGammaToTargetSpace(v.vertexColor);
				o.pos = UnityObjectToClipPos(v.vertex);
				return o;
			}

			float4 frag (VertexOutput i) : SV_Target {
				float4 rawColor = tex2D(_MainTex,i.uv);
				float finalAlpha = (rawColor.a * i.vertexColor.a);

				#if defined(_STRAIGHT_ALPHA_INPUT)
				rawColor.rgb *= rawColor.a;
				#endif

				float3 finalColor = lerp((rawColor.rgb * i.vertexColor.rgb), (_FillColor.rgb * finalAlpha), _FillPhase); // make sure to PMA _FillColor.
				return fixed4(finalColor, finalAlpha);
			}
			ENDCG
		}

		Pass {
			Name "Caster"
			Tags { "LightMode"="ShadowCaster" }
			Offset 1, 1
			ZWrite On
			ZTest LEqual

			Fog { Mode Off }
			Cull Off
			Lighting Off

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_shadowcaster
			#pragma fragmentoption ARB_precision_hint_fastest
			#include "UnityCG.cginc"
			sampler2D _MainTex;
			fixed _Cutoff;

			struct VertexOutput {
				V2F_SHADOW_CASTER;
				float4 uvAndAlpha : TEXCOORD1;
			};

			VertexOutput vert (appdata_base v, float4 vertexColor : COLOR) {
				VertexOutput o;
				o.uvAndAlpha = v.texcoord;
				o.uvAndAlpha.a = vertexColor.a;
				TRANSFER_SHADOW_CASTER(o)
				return o;
			}

			float4 frag (VertexOutput i) : SV_Target {
				fixed4 texcol = tex2D(_MainTex, i.uvAndAlpha.xy);
				clip(texcol.a * i.uvAndAlpha.a - _Cutoff);
				SHADOW_CASTER_FRAGMENT(i)
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
	CustomEditor "SpineShaderWithOutlineGUI"
}

 在这里可以先试用GPU-instancing验证数值是否正确,因为上面也说过GPU-instancing由于动画中的网格不一致导致并不能合批,但这里仅用于验证图集uv映射的正确性,下为测试脚本:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Game.Core;
using Spine.Unity;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.PlayerLoop;
using UnityEngine.U2D;

public class TestScript : MonoBehaviour
{
    public SpriteAtlas spriteAtlas;
    public Material commonMaterial;
    public List<OneUnit> oneUnits = new List<OneUnit>();

    MaterialPropertyBlock materialPropertyBlock = new();

    [Serializable]
    public class OneUnit{
        public string textureName;
        public MeshRenderer meshRenderer;
        public MeshFilter meshFilter;
        public SkeletonAnimation skeletonAnimation;
    }

    /*
    // var border = sprite.border;  //  边界大小 (0,0,0,0)
    // var bounds = sprite.bounds;  //  边界 center(0,0,0,0) extends(3.38,3.38,0.10) max(3.38,3.38,0.10) min(-3.38,-3.38,-0.10) size(6.76,6.76,0.20)
    // var packed = sprite.packed;  //  是否被打包到图集中 true
    // var packingMode = sprite.packingMode;  //  图集打包方式 Rectangle
    // var packingRotation = sprite.packingRotation;  //  图集打包旋转 None
    // var pivot = sprite.pivot;  //  在原始纹理矩形中的位置 x:256 y:256
    // var pixelsPerUnit = sprite.pixelsPerUnit;  //  每单位像素数  75.73965
    // var rect = sprite.rect;  //  原始纹理上精灵的位置 center(256,256) width:512 height:512
    // var spriteAtlasTextureScale = sprite.spriteAtlasTextureScale;  //  纹理的变体比例 1
    // var textureRect = sprite.textureRect;  //  纹理上使用的矩形 center(1110,256) position(854,0) max(1366,512) min(854,0) x:854 y:0 size(512,512)
    // var textureRectOffset = sprite.textureRectOffset;  //  纹理上使用的矩形偏移量 x:0 y:0
    */

    private void Start()
    {
        Texture2D texture2D = null;
        
        foreach(var oneUnit in oneUnits){
            
            //  目标图片精灵
            Sprite sprite = spriteAtlas.GetSprite(oneUnit.textureName);
            
            //  整个图集的texture2D
            if(texture2D == null){
                texture2D = sprite.texture;
                commonMaterial.SetTexture("_MainTex", texture2D);
            }

            //  对于已知要采样的uv1点(u1,v1)  可以计算出这个点相对于整个texture左下角的位置(x1,y1)  [需传入整张texture的宽高]  
            //  根据这个已知要采样的uv1点(u1,v1)  可以计算出换算后的点相对于精灵矩形左下角的位置(x2,y2)  [需传入精灵texture的宽高] 
            //  再加上精灵矩形左下角的坐标,计算出换算点的位置(x3,y3)  [需传入精灵texture左下角相对于整个texture的位置坐标]
            //  根据换算点的位置(x3,y3)  可以计算出换算点在整个texture的uv

            Vector2 textureWidthAndHeight = new Vector2(texture2D.width, texture2D.height);  //  整张texture的宽高
            Vector2 spriteTextureWidthAndHeight = new Vector2(sprite.textureRect.width, sprite.textureRect.height);  //  精灵texture的宽高
            Vector2 spriteTextureLeftDownPos = sprite.textureRect.position;  //  精灵texture左下角相对于整个texture的位置坐标

            materialPropertyBlock = new();
            materialPropertyBlock.SetVector("textureWidthAndHeight", new Vector4(textureWidthAndHeight.x, textureWidthAndHeight.y, 0, 0));
            materialPropertyBlock.SetVector("spriteTextureWidthAndHeight", new Vector4(spriteTextureWidthAndHeight.x, spriteTextureWidthAndHeight.y, 0, 0));
            materialPropertyBlock.SetVector("spriteTextureLeftDownPos", new Vector4(spriteTextureLeftDownPos.x, spriteTextureLeftDownPos.y, 0, 0));
            oneUnit.meshRenderer.SetPropertyBlock(materialPropertyBlock);
            
            commonMaterial.SetTexture("_MainTex", texture2D);
            
            var originalMaterial = oneUnit.skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial;
            oneUnit.skeletonAnimation.CustomMaterialOverride[originalMaterial] = commonMaterial; // spine的特殊材质替换,可以去官方文档查看详情
            
            
        
        }
    }

}

以下为使用GPU-instancing的shader文件

Shader "CustomURP/SpineTestShader" {
	Properties {

	}
	
	SubShader {
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
		Blend One OneMinusSrcAlpha
		Cull Off
		ZWrite Off
		Lighting Off

		Stencil {
			Ref 1
			Comp Always
			Pass Keep
		}

		Pass {
			Name "Normal"

			CGPROGRAM

			#pragma multi_compile_instancing
			#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"
			#include "Spine-Common.cginc"
			
			sampler2D _MainTex;
			
			UNITY_INSTANCING_BUFFER_START(Props)
	           UNITY_DEFINE_INSTANCED_PROP(vector, textureWidthAndHeight)
	           UNITY_DEFINE_INSTANCED_PROP(vector, spriteTextureWidthAndHeight)
	           UNITY_DEFINE_INSTANCED_PROP(vector, spriteTextureLeftDownPos)
	        UNITY_INSTANCING_BUFFER_END(Props)

			//  把要采样的uv转换到图集上对应的uv位置,这样才能正确采样图片
			float2 ConvertUVToSamplerUV(float2 uv, float2 textureWidthAndHeight, float2 spriteTextureWidthAndHeight, float2 spriteTextureLeftDownPos){
				float2 uv1 = uv;
				//  对于已知要采样的uv1点(u1,v1)  可以计算出这个点相对于整个texture左下角的位置(x1,y1)  [需传入整张texture的宽高]  
				float2 uv1PosRefFullTexture = float2(uv1.x * textureWidthAndHeight.x, uv1.y * textureWidthAndHeight.y);
				//  根据这个已知要采样的uv1点(u1,v1)  可以计算出换算后的点相对于精灵矩形左下角的位置(x2,y2)  [需传入精灵texture的宽高] 
				float2 uv2PosRefSpriteTexture = float2(uv1.x * spriteTextureWidthAndHeight.x, uv1.y * spriteTextureWidthAndHeight.y);
				//  再加上精灵矩形左下角的坐标,计算出换算点的位置  [需传入精灵texture左下角相对于整个texture的位置坐标]
				float2 uv2PosRefFullTexture = uv2PosRefSpriteTexture + spriteTextureLeftDownPos;
				//  根据换算点的位置  可以计算出换算点在整个texture的uv
				float2 uv2 = float2(uv2PosRefFullTexture.x / textureWidthAndHeight.x, uv2PosRefFullTexture.y / textureWidthAndHeight.y);
				return uv2;
			}
			
			struct VertexInput {
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
				UNITY_VERTEX_INPUT_INSTANCE_ID
			};

			struct VertexOutput {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
				UNITY_VERTEX_INPUT_INSTANCE_ID
			};

			VertexOutput vert (VertexInput v) {
				
				VertexOutput o = (VertexOutput)0;

				UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

				o.uv = ConvertUVToSamplerUV(v.uv, UNITY_ACCESS_INSTANCED_PROP(Props, textureWidthAndHeight),
					UNITY_ACCESS_INSTANCED_PROP(Props, spriteTextureWidthAndHeight), UNITY_ACCESS_INSTANCED_PROP(Props, spriteTextureLeftDownPos));
				o.vertexColor = PMAGammaToTargetSpace(v.vertexColor);
				o.pos = UnityObjectToClipPos(v.vertex);

				return o;
			}

			float4 frag (VertexOutput i) : SV_Target {

				UNITY_SETUP_INSTANCE_ID(i);

				float4 rawColor = tex2D(_MainTex,i.uv);
				float finalAlpha = (rawColor.a * i.vertexColor.a);

				#if defined(_STRAIGHT_ALPHA_INPUT)
				rawColor.rgb *= rawColor.a;
				#endif

				float3 finalColor = lerp((rawColor.rgb * i.vertexColor.rgb), finalAlpha, 0);
				
				return fixed4(finalColor, finalAlpha);
			}
			ENDCG
		}

	}
	FallBack "Diffuse"
	CustomEditor "SpineShaderWithOutlineGUI"
}

可见已经验证计算成功,所有的spine都是用的一张图集计算自身的uv做采样,贴图显示正常,但由于网格问题无法合批,还是分成了五次绘制。

 接下来转为使用SRP合批,关于如何使用SRP合批,在官网上有详细的说明

Unity - Manual: GPU instancing

 这里简单说明,就是三个要求:

1:着色器必须在名为UnityPerDraw的单个常量缓冲区中声明所有内置引擎属性

2:着色器必须在名为UnityPerMaterial单个常量缓冲区中声明所有材质属性。

3:不能使用MaterialPropertyBlock,他与GPU-instancing冲突且优先级更高。

但是不能使用MaterialPropertyBlock会带来新的问题,不能给每个材质实例的变量单独赋值,就不能通过上面GPU-instancing的方法传入那三个变量去确定采样uv。

解决思路:通过修改spine插件代码,在生成网格时把这三个变量写入到顶点数据中,最后shader从顶点数据获取并计算最终的采样uv。

以下为改造为SRP合批的shader文件

Shader "CustomURP/SpineTestShader" {
	Properties {

	}
	
	SubShader {
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
		Blend One OneMinusSrcAlpha
		Cull Off
		ZWrite Off
		Lighting Off

		Stencil {
			Ref 1
			Comp Always
			Pass Keep
		}

		Pass {
			Name "Normal"

			CGPROGRAM

			#pragma multi_compile_instancing
			#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"
			#include "Spine-Common.cginc"
			
			CBUFFER_START(UnityPerMaterial)
			sampler2D _MainTex;
			CBUFFER_END

			//  把要采样的uv转换到图集上对应的uv位置,这样才能正确采样图片
			float2 ConvertUVToSamplerUV(float2 uv, float2 textureWidthAndHeight, float2 spriteTextureWidthAndHeight, float2 spriteTextureLeftDownPos){
				float2 uv1 = uv;
				//  对于已知要采样的uv1点(u1,v1)  可以计算出这个点相对于整个texture左下角的位置(x1,y1)  [需传入整张texture的宽高]  
				float2 uv1PosRefFullTexture = float2(uv1.x * textureWidthAndHeight.x, uv1.y * textureWidthAndHeight.y);
				//  根据这个已知要采样的uv1点(u1,v1)  可以计算出换算后的点相对于精灵矩形左下角的位置(x2,y2)  [需传入精灵texture的宽高] 
				float2 uv2PosRefSpriteTexture = float2(uv1.x * spriteTextureWidthAndHeight.x, uv1.y * spriteTextureWidthAndHeight.y);
				//  再加上精灵矩形左下角的坐标,计算出换算点的位置  [需传入精灵texture左下角相对于整个texture的位置坐标]
				float2 uv2PosRefFullTexture = uv2PosRefSpriteTexture + spriteTextureLeftDownPos;
				//  根据换算点的位置  可以计算出换算点在整个texture的uv
				float2 uv2 = float2(uv2PosRefFullTexture.x / textureWidthAndHeight.x, uv2PosRefFullTexture.y / textureWidthAndHeight.y);
				return uv2;
			}
			
			struct VertexInput {
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
				float2 textureWidthAndHeight : TEXCOORD4;
				float2 spriteTextureWidthAndHeight : TEXCOORD5;
				float2 spriteTextureLeftDownPos : TEXCOORD6;
			};

			struct VertexOutput {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 vertexColor : COLOR;
			};

			VertexOutput vert (VertexInput v) {

				VertexOutput o = (VertexOutput)0;

				o.uv = ConvertUVToSamplerUV(v.uv, v.textureWidthAndHeight, v.spriteTextureWidthAndHeight, v.spriteTextureLeftDownPos);
				o.vertexColor = PMAGammaToTargetSpace(v.vertexColor);
				o.pos = UnityObjectToClipPos(v.vertex);

				return o;
			}

			float4 frag (VertexOutput i) : SV_Target {

				float4 rawColor = tex2D(_MainTex,i.uv);
				float finalAlpha = (rawColor.a * i.vertexColor.a);

				#if defined(_STRAIGHT_ALPHA_INPUT)
				rawColor.rgb *= rawColor.a;
				#endif

				float3 finalColor = lerp((rawColor.rgb * i.vertexColor.rgb), finalAlpha, 0);
				
				return fixed4(finalColor, finalAlpha);
			}
			ENDCG
		}

	}
	FallBack "Diffuse"
	CustomEditor "SpineShaderWithOutlineGUI"
}

可见顶点数据中多了三个变量用于计算最终采样uv。所有材质属性都声明在既定位置。CBUFFER_START(UnityPerMaterial)
材质属性
CBUFFER_END

而对于spine插件的改造主要集中在MeshGenerator脚本中的FillVertexData方法,这里把改造后的方法贴出:

public void FillVertexData (Mesh mesh, bool isUnit, 
		Vector2 textureWidthAndHeight = default,//  整张texture的宽高
        Vector2 spriteTextureWidthAndHeight = default,//  精灵texture的宽高
        Vector2 spriteTextureLeftDownPos = default)//  精灵texture左下角相对于整个texture的位置坐标
		{
			Vector3[] vbi = vertexBuffer.Items;
			Vector2[] ubi = uvBuffer.Items;
			Color32[] cbi = colorBuffer.Items;
			int vbiLength = vbi.Length;

			// Zero the extra.
			{
				int listCount = vertexBuffer.Count;
				Vector3 vector3zero = Vector3.zero;
				for (int i = listCount; i < vbiLength; i++)
					vbi[i] = vector3zero;
			}

			// Set the vertex buffer.
			{
				mesh.vertices = vbi;
				mesh.uv = ubi;
				mesh.colors32 = cbi;
				mesh.bounds = GetMeshBounds();
			}

			{
				if (settings.addNormals) {
					int oldLength = 0;

					if (normals == null)
						normals = new Vector3[vbiLength];
					else
						oldLength = normals.Length;

					if (oldLength != vbiLength) {
						Array.Resize(ref this.normals, vbiLength);
						Vector3[] localNormals = this.normals;
						for (int i = oldLength; i < vbiLength; i++) localNormals[i] = Vector3.back;
					}
					mesh.normals = this.normals;
				}

				if (settings.tintBlack) {
					if (uv2 != null) {
						// Sometimes, the vertex buffer becomes smaller. We need to trim the size of the tint black buffers to match.
						if (vbiLength != uv2.Items.Length) {
							Array.Resize(ref uv2.Items, vbiLength);
							Array.Resize(ref uv3.Items, vbiLength);
							uv2.Count = uv3.Count = vbiLength;
						}
						mesh.uv2 = this.uv2.Items;
						mesh.uv3 = this.uv3.Items;
					}
				}
			}

			{
				//  如果是单位,进行额外合批处理
				if(isUnit){  
					
					//  保证数组大小
					if(textureWidthAndHeights.Length != vbiLength){
						textureWidthAndHeights = new Vector2[vbiLength];
					}
					if(spriteTextureWidthAndHeights.Length != vbiLength){
						spriteTextureWidthAndHeights = new Vector2[vbiLength];
					}
					if(spriteTextureLeftDownPoss.Length != vbiLength){
						spriteTextureLeftDownPoss = new Vector2[vbiLength];
					}

					for(int i = 0; i < vbiLength; i++){
						textureWidthAndHeights[i] = textureWidthAndHeight;
						spriteTextureWidthAndHeights[i] = spriteTextureWidthAndHeight;
						spriteTextureLeftDownPoss[i] = spriteTextureLeftDownPos;
					}

					mesh.uv5 = textureWidthAndHeights;
					mesh.uv6 = spriteTextureWidthAndHeights;
					mesh.uv7 = spriteTextureLeftDownPoss;
				}
			}
		}

改动代码为这些:

这三个值传入的位置为uv5,uv6和uv7,但是在shader中则是使用TEXCOORD4,TEXCOORD5,TEXCOORD6接收

float2 textureWidthAndHeight : TEXCOORD4;
float2 spriteTextureWidthAndHeight : TEXCOORD5;
float2 spriteTextureLeftDownPos : TEXCOORD6;

这是因为mesh中没有uv0只有uv1~8,而TEXCOORD对应的则为0~7

接下来是把数值传输给spine让他在生成网格时对应赋值到顶点数据即可。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Game.Core;
using Spine.Unity;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.PlayerLoop;
using UnityEngine.U2D;

public class TestScript : MonoBehaviour
{
    public SpriteAtlas spriteAtlas;
    public Material commonMaterial;
    public List<OneUnit> oneUnits = new List<OneUnit>();
    
    [Serializable]
    public class OneUnit{
        public string textureName;
        public MeshRenderer meshRenderer;
        public MeshFilter meshFilter;
        public SkeletonAnimation skeletonAnimation;
    }

    /*
    // var border = sprite.border;  //  边界大小 (0,0,0,0)
    // var bounds = sprite.bounds;  //  边界 center(0,0,0,0) extends(3.38,3.38,0.10) max(3.38,3.38,0.10) min(-3.38,-3.38,-0.10) size(6.76,6.76,0.20)
    // var packed = sprite.packed;  //  是否被打包到图集中 true
    // var packingMode = sprite.packingMode;  //  图集打包方式 Rectangle
    // var packingRotation = sprite.packingRotation;  //  图集打包旋转 None
    // var pivot = sprite.pivot;  //  在原始纹理矩形中的位置 x:256 y:256
    // var pixelsPerUnit = sprite.pixelsPerUnit;  //  每单位像素数  75.73965
    // var rect = sprite.rect;  //  原始纹理上精灵的位置 center(256,256) width:512 height:512
    // var spriteAtlasTextureScale = sprite.spriteAtlasTextureScale;  //  纹理的变体比例 1
    // var textureRect = sprite.textureRect;  //  纹理上使用的矩形 center(1110,256) position(854,0) max(1366,512) min(854,0) x:854 y:0 size(512,512)
    // var textureRectOffset = sprite.textureRectOffset;  //  纹理上使用的矩形偏移量 x:0 y:0
    */

    private void Start()
    {
        Texture2D texture2D = null;
        
        foreach(var oneUnit in oneUnits){
            
            //  目标图片精灵
            Sprite sprite = spriteAtlas.GetSprite(oneUnit.textureName);  //  2002
            
            //  整个图集的texture2D
            if(texture2D == null){
                texture2D = sprite.texture;
                commonMaterial.SetTexture("_MainTex", texture2D);  //  贴图无法使用materialPropertyBlock传递
            }

            //  对于已知要采样的uv1点(u1,v1)  可以计算出这个点相对于整个texture左下角的位置(x1,y1)  [需传入整张texture的宽高]  
            //  根据这个已知要采样的uv1点(u1,v1)  可以计算出换算后的点相对于精灵矩形左下角的位置(x2,y2)  [需传入精灵texture的宽高] 
            //  再加上精灵矩形左下角的坐标,计算出换算点的位置(x3,y3)  [需传入精灵texture左下角相对于整个texture的位置坐标]
            //  根据换算点的位置(x3,y3)  可以计算出换算点在整个texture的uv

            Vector2 textureWidthAndHeight = new Vector2(texture2D.width, texture2D.height);  //  整张texture的宽高
            Vector2 spriteTextureWidthAndHeight = new Vector2(sprite.textureRect.width, sprite.textureRect.height);  //  精灵texture的宽高
            Vector2 spriteTextureLeftDownPos = sprite.textureRect.position;  //  精灵texture左下角相对于整个texture的位置坐标
            
            var originalMaterial = oneUnit.skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial;
            oneUnit.skeletonAnimation.CustomMaterialOverride[originalMaterial] = commonMaterial; // to enable the replacement.
            
            commonMaterial.SetTexture("_MainTex", texture2D);
        
            oneUnit.skeletonAnimation.isUnit = true;
            oneUnit.skeletonAnimation.textureWidthAndHeight = textureWidthAndHeight;
            oneUnit.skeletonAnimation.spriteTextureWidthAndHeight = spriteTextureWidthAndHeight;
            oneUnit.skeletonAnimation.spriteTextureLeftDownPos = spriteTextureLeftDownPos;
        
        }
    }

}

这样所有的spine就可以合并到一批去了,当然注意在配置中开启SRP合批,就在Dynamic合批的上方。

 而想要使用Dynamic合批和很简单,只要声明一个变量在外并在代码中使用即可。可能是不使用会被自动剔除,不会破坏SRP合批。

这样就会破坏SRP合批以Dynamic合批运行。

但是Dynamic合批除了必须要求材质相同以外还有一些限制:

1:Dynamic合批无法应用于包含超过 900 个顶点属性和 300 个顶点的网格

2:多通道着色器的游戏对象无法Dynamic合批

3:旧版延迟渲染则不支持Dynamic合批

而在使用着两个合批方式的时候,要根据具体情况测试,我这里有一些简单的测试参考,Dynamic合批在复杂情况下Batches更少但是SetPass calls可能会更多,而SRP合批在复杂情况下Batches更多但SetPass calls可能会较少,根据普遍认知SetPass calls的耗时会更长但并不是绝对的,具体选用哪种还是要根据实际情况测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953517.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Elasticsearch】批量操作:优化性能

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探…

深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比

众所周知&#xff0c;不管是什么框架&#xff0c;在前端 UI 渲染时&#xff0c;都会有构造出一套相关的渲染树&#xff0c;并且在 UI 更新时&#xff0c;为了尽可能提高性能&#xff0c;一般都只会进行「差异化」更新&#xff0c;而不是对整个 UI Tree 进行刷新&#xff0c;所以…

Docker 的安装和基本使用[SpringBoot之Docker实战系列] - 第535篇

历史文章&#xff08;文章累计530&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 《…

介绍下不同语言的异常处理机制

Golang 在Go语言中&#xff0c;有两种用于处于异常的机制&#xff0c;分别是error和panic&#xff1b; panic panic 是 Go 中处理异常情况的机制&#xff0c;用于表示程序遇到了无法恢复的错误&#xff0c;需要终止执行。 使用场景 程序出现严重的不符合预期的问题&#x…

车联网安全--TLS握手过程详解

目录 1. TLS协议概述 2. 为什么要握手 2.1 Hello 2.2 协商 2.3 同意 3.总共握了几次手&#xff1f; 1. TLS协议概述 车内各ECU间基于CAN的安全通讯--SecOC&#xff0c;想必现目前多数通信工程师们都已经搞的差不多了&#xff08;不要再问FvM了&#xff09;&#xff1b;…

【update 更新数据语法合集】.NET开源ORM框架 SqlSugar 系列

系列文章目录 &#x1f380;&#x1f380;&#x1f380; .NET开源 ORM 框架 SqlSugar 系列 &#x1f380;&#x1f380;&#x1f380; 文章目录 系列文章目录前言 &#x1f343;一、实体对象更新1.1 单条与批量1.2 不更新某列1.3 只更新某列1.4 NULL列不更新1.5 无主键/指定列…

51单片机入门基础

目录 一、基础知识储备 &#xff08;一&#xff09;了解51单片机的基本概念 &#xff08;二&#xff09;掌握数字电路基础 &#xff08;三&#xff09;学习C语言编程基础 二、开发环境搭建 &#xff08;一&#xff09;硬件准备 &#xff08;二&#xff09;软件准备 三、…

22、PyTorch nn.Conv2d卷积网络使用教程

文章目录 1. 卷积2. python 代码3. notes 1. 卷积 输入A张量为&#xff1a; A [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ] \begin{equation} A\begin{bmatrix} 0&1&2&3\\\\ 4&5&6&7\\\\ 8&9&10&11\\\\ 12&13&14&15 \end{b…

Python爬虫-汽车之家各车系周销量榜数据

前言 本文是该专栏的第43篇,后面会持续分享python爬虫干货知识,记得关注。 在本专栏之前,笔者在文章《Python爬虫-汽车之家各车系月销量榜数据》中,有详细介绍,如何爬取“各车系车型的月销量榜单数据”的方法以及完整代码教学教程。 而本文,笔者同样以汽车之家平台为例,…

web前端第五次作业---制作菜单

制作菜单 代码: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style…

个人曾经ARM64_汇编角度_PLTHOOK的研究

ARM64基础HOOK研究_2024 之前为了实现一个修改器变速器的小功能,结果研究了很多关于ELF的内容,特别是so文件(ARM64的) 还研究了Hook,以及注入进程等操作,以及实现类似IDA那样的断点,汇编转换,以及软硬断点等(实现了CE那种谁写入/访问/读取的检测),这里就不作记录了,只记录一下简…

win10 Outlook(new) 企业邮箱登录 登录失败。请在几分钟后重试。

windows系统经常弹出使用Outlook(new&#xff09;&#xff0c;自动切过去。 但是登录企业的内网邮箱&#xff0c;折腾了好几次都使用不了。排查网络等问题&#xff0c;在社区找到了答案。 推出一年多不支持企业账户&#xff0c;所以之前的折腾都是浪费时间。 因为这个答案不太…

MySQL中的四种表联结

目录 1、联结、关系表 &#xff08;1&#xff09;关系表 &#xff08;2&#xff09;为什么使用联结 2、如何创建联结 &#xff08;1&#xff09;笛卡尔积&#xff08;叉联结&#xff09;--用逗号分隔 &#xff08;2&#xff09;where子句的重要性 &#xff08;3&#xff…

【Oracle专栏】group by 和distinct 效率

Oracle相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 1.背景 查阅资料&#xff1a; 1&#xff09;有索引情况下&#xff0c;group by和distinct都能使用索引&#xff0c;效率相同。 2&#xff09;无索引情况下&#xff0c;distinct…

easyui datagrid表头和网格错位问题

问题&#xff1a;表头与数据网格错位 解决&#xff1a; 在onLoadSuccess事件中调用fitColumns方法 $(this).datagrid(‘fitColumns’);

[文献精汇]使用 LSTM Networks 的均值回归交易策略

Backtrader 策略实例 [Backtrader]实例:均线策略[Backtrader] 实例:MACD策略[Backtrader] 实例:KDJ 策略[Backtrader] 实例:RSI 与 EMA 结合[Backtrader] 实例:SMA自定义数据源[Backtrader] 实例:海龟策略[Backtrader] 实例:网格交易[Backtrader] 实例: 配对交[Backtrader] 机…

DBeaver执行本地的sql语句文件避免直接在客户端运行卡顿

直接在客户端运行 SQL 语句和通过加载本地文件执行 SQL 语句可能会出现不同的性能表现&#xff0c;原因可能包括以下几点&#xff1a; 客户端资源使用&#xff1a; 当你在客户端界面直接输入和执行 SQL 语句时&#xff0c;客户端可能会消耗资源来维护用户界面、语法高亮、自动完…

opencv warpAffine仿射变换C++源码分析

基于opencv 3.1.0源代码 sources\modules\imgproc\src\imgwarp.cpp void cv::warpAffine( InputArray _src, OutputArray _dst,InputArray _M0, Size dsize,int flags, int borderType, const Scalar& borderValue ) {...if( !(flags & WARP_INVERSE_MAP) ){//变换矩阵…

【数字化】华为-用变革的方法确保规划落地

导读&#xff1a;华为在数字化转型过程中&#xff0c;深刻认识到变革的必要性&#xff0c;并采用了一系列有效的方法确保转型规划的有效落地。华为认为&#xff0c;数字化转型不仅仅是技术层面的革新&#xff0c;更是企业运作模式、流程、组织、文化等深层次的变革。数字化转型…

Excel中SUM求和为0?难道是Excel有Bug!

大家好&#xff0c;我是小鱼。 在日常工作中有时会遇到这样的情况&#xff0c;对Excel表格数据进行求和时&#xff0c;结果竟然是0&#xff0c;很多小伙伴甚至都怀疑是不是Excel有Bug&#xff01;其实&#xff0c;在WPS的Excel表格中数据求和&#xff0c;结果为0无法正确求和的…