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

news2025/1/15 18:46:13

对于导入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的耗时会更长但并不是绝对的,具体选用哪种还是要根据实际情况测试。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

list的迭代器模拟实现和迭代器失效(续)

文章目录 list的迭代器operator->普通迭代器和const迭代器迭代器模版迭代器失效析构拷贝构造赋值重载 initializer_list list的迭代器 对迭代器进行封装&#xff0c;迭代器的指针还是4个字节&#xff0c;在物理上是一样的&#xff0c;但是底层是完全不同的 迭代器是浅拷贝&a…

一文通透OpenVLA及其源码剖析——基于Prismatic VLM(SigLIP、DinoV2、Llama 2)及离散化动作预测

前言 当对机器人动作策略的预测越来越成熟稳定之后(比如ACT、比如扩散策略diffusion policy)&#xff0c;为了让机器人可以拥有更好的泛化能力&#xff0c;比较典型的途径之一便是基于预训练过的大语言模型中的广泛知识&#xff0c;然后加一个policy head(当然&#xff0c;一开…

Easysearch Rollup 使用指南

背景 在现代数据驱动的世界中&#xff0c;时序数据的处理变得越来越重要。无论是监控系统、日志分析&#xff0c;还是物联网设备的数据收集&#xff0c;时序数据都占据了大量的存储空间。随着时间的推移&#xff0c;这些数据的存储成本和管理复杂度也在不断增加。 为了解决这…

FPGA EDA软件的位流验证

位流验证&#xff0c;对于芯片研发是一个非常重要的测试手段&#xff0c;对于纯软件开发人员&#xff0c;最难理解的就是位流验证。在FPGA芯片研发中&#xff0c;位流验证是在做什么&#xff0c;在哪些阶段需要做位流验证&#xff0c;如何做&#xff1f;都是问题。 我们先整体的…

[免费]SpringBoot+Vue新能源汽车充电桩管理系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue新能源汽车充电桩管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue新能源汽车充电桩管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 随着信息化时代的到来&#xff0…

如何选择多个视频文件

文章目录 1. 概念介绍2. 方法与细节2.1 实现方法2.2 具体细节 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何选择多个图片文件"相关的内容&#xff0c;本章回中将介绍如何选择视频文件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在前…

闲谭SpringBoot--ShardingSphere分布式事务探究

文章目录 0. 背景1. 未分库分表时2. 仅分表时3. 分库分表时3.1 不涉及分库表3.2 涉及分库表&#xff0c;且分库表处于一个库3.3 涉及分库表&#xff0c;且分库表处于多个库3.4 涉及分库表&#xff0c;且运行中某库停机 4. 小结 0. 背景 接上篇文章《闲谭SpringBoot–ShardingS…

【20250113】基于肌肉形变测量的连续步态相位估计算法,可自适应步行速度和地形坡度...

【基本信息】 论文标题&#xff1a;Continuous Gait Phase Estimation by Muscle Deformations with Speed and Ramp Adaptability 发表期刊&#xff1a;IEEE Sensors Journal 发表时间&#xff1a;2024年5月30日 【访问链接】 论文链接&#xff1a;https://ieeexplore.ieee.or…

快速上手 HarmonyOS 应用开发

一、DevEco Studio 安装与配置 1. DevEco Studio 简介 DevEco Studio 是 HarmonyOS 的一站式集成开发环境&#xff08;IDE&#xff09;&#xff0c;提供了丰富的工具和功能&#xff0c;支持 HarmonyOS 应用开发的全流程。 2. DevEco Studio 下载与安装 下载地址&#xff1a…

Vue如何构建项目

目录 1.安装Node.js 2.换源(建议) 3.选择一个目录 4.创建一个vue项目 5.验证是否成功 1.安装Node.js 安装18.3或更⾼版本的 Nodejs 点击下载->Node.Js中文网 node -v npm -v 安装好后在windows的cmd窗口下运行 如果能运行出结果就说明安装好了。 2.换源(建议) //…

HTML拖拽功能(纯html5+JS实现)

1、HTML拖拽--单元行拖动 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><…

初识算法和数据结构P1:保姆级图文详解

文章目录 前言1、算法例子1.1、查字典&#xff08;二分查找算法&#xff09;1.2、整理扑克&#xff08;插入排序算法&#xff09;1.3、货币找零&#xff08;贪心算法&#xff09; 2、算法与数据结构2.1、算法定义2.2、数据结构定义2.3、数据结构与算法的关系2.4、独立于编程语言…

Linux 多路径Multipath学习

文章目录 Linux 多路径Multipath学习1. 简介1.1 Multipath I/O 技术 2. Multipath安装与使用2.1 安装2.2 启动 3. scsi设备模拟器scsi_debug3.1 简介3.2 加载 scsi_debug3.3 查看 scsi_id3.4 配置多个模拟设备3.5 聚合多路径 4. 删除聚合和单设备4.1 删除mpath4.2 删除 scsi 设…

浅谈云计算06 | 云管理系统架构

云管理系统架构 一、云管理系统架构&#xff08;一&#xff09;远程管理系统&#xff08;二&#xff09;资源管理系统&#xff08;三&#xff09;SLA 管理系统&#xff08;四&#xff09;计费管理系统 二、安全与可靠性保障&#xff08;一&#xff09;数据安全防线&#xff08;…

Observability:利用 GCP Vertex AI 集成提升 LLM 可观察性

作者&#xff1a;来自 Elastic Ishleen Kaur•Muthukumar Paramasivam 随着组织越来越多地将 LLM 用于内容创建、检索增强生成 (Retrieval-Augmented Generation - RAG) 和数据分析等 AI 应用&#xff0c;SRE 和开发人员面临着新的挑战。监控工作流、分析输入和输出、管理查询延…

Node.js - Express框架

1. 介绍 Express 是一个基于 Node.js 的 Web 应用程序框架&#xff0c;主要用于快速、简便地构建 Web 应用程序 和 API。它是目前最流行的 Node.js Web 框架之一&#xff0c;具有轻量级、灵活和功能丰富的特点。 核心概念包括路由&#xff0c;中间件&#xff0c;请求与响应&a…

Linux Top 命令 load average 指标解读

前言 作为平台开发的同学&#xff0c;维护平台稳定性是我们最基本的工作职责&#xff0c;下面主要介绍下top 命令里 &#xff0c;load average 这个指标如何去衡量机器负载程度。 概念介绍 load average 是系统在过去 1 分钟、5 分钟、15 分钟 的平均负载&#xff0c;它表示运…

【大数据】机器学习------神经网络模型

一、神经网络模型 1. 基本概念 神经网络是一种模拟人类大脑神经元结构的计算模型&#xff0c;由多个神经元&#xff08;节点&#xff09;组成&#xff0c;这些节点按照不同层次排列&#xff0c;通常包括输入层、一个或多个隐藏层和输出层。每个神经元接收来自上一层神经元的输…

【day5】Redis持久化之AOF + Redis事务_锁机制

AOF是什么 以日志的形式来记录每个写操作(增量保存)&#xff0c;将 Redis 执行过的所有写指令记录下来(比 如 set/del 操作会记录, 读操作 get 不记录 只许追加文件但不可以改写文件 redis 启动之初会读取该文件重新构建数据 redis 重启的话就根据日志文件的内容将写指令从前到…

【Python】Python之locust压测教程+从0到1demo:基础轻量级压测实战(1)

文章目录 一、什么是Locust二、Locust 架构组成三、实战 Demo准备一个可调用的接口编写一个接口测试用例编写一个性能测试用例执行性能测试用例代码1、通过 Web UI 执行&#xff08;GUI模式&#xff09;2、通过命令行执行&#xff08;非GUI模式&#xff09; 小知识&#xff1a;…