Combining 4 villains into one, using gpu Instance to batch draw can use the characteristics of gpu Instance to reduce batch processing, but gpu Instance only supports mesh filter, so it is necessary to bake the skin animation into vertex animation, and pass the value to the shader to distinguish the display through MaterialPropertyBlock characters.
Baking character and animation code
using System; using System.IO; using System. Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; [CustomEditor(typeof(BakingAnimMesh))] public class BakingAnimMeshEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); BakingAnimMesh myScript = (BakingAnimMesh) target; if (GUILayout.Button("Extract")) { myScript. SaveAsset(); } } } public class BakingAnimMesh : MonoBehaviour { public AnimationClip clip;//Specify the animation to be baked public Animator[] animator;//characters with animation components public SkinnedMeshRenderer[] sk;//The skinned mesh renderer of animated characters int pnum;//total pixels Texture2D texture; int size = 0;//The width and height of the image int vertexCount = 0;//total number of vertices int frameCount;//total number of frames public string path = "mini";//export name //Export animated pictures, merged grids, and merged textures public void SaveAsset() { //Total number of frames = animation duration * frame rate frameCount = Mathf. CeilToInt(clip. length * 30); Debug. Log("frameCount:" + frameCount); // number of vertices int[] vertexNum = new int[sk. Length + 1]; vertexNum[0] = 0; for (int i = 0; i < sk. Length; i ++ ) { vertexCount + = sk[i].sharedMesh.vertexCount; vertexNum[i + 1] = vertexCount; } Debug. Log("vertexCount:" + vertexCount); //total pixels = total number of vertices * total number of frames pnum = vertexCount * frameCount; Debug. Log("pnum:" + pnum); //Convert the number of pixels into a texture width and height that is a multiple of 2 size = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(pnum))); Debug. Log("size:" + size); texture = new Texture2D(size, size, TextureFormat. RGBAFloat, false, true); //Calculate the maximum and minimum values of the vertices _CalculateVertexMinAndMax(out float min, out float max); Debug. Log("min:" + min); Debug. Log("max:" + max); //Baking picture _BakeAnimationClip(max, min); //Merge and export meshes Mesh mesh = new Mesh(); //Merge grid textures List<Texture2D> textures = new List<Texture2D>(); for (int i = 0; i < sk. Length; i ++ ) { textures.Add(sk[i].sharedMaterial.mainTexture as Texture2D); } Texture2D meshtexture = new Texture2D(2048, 2048, TextureFormat. RGBAFloat, false, true); Rect[] rects = meshtexture. PackTextures(textures. ToArray(), 0); meshtexture. Apply(); //merge grid List<CombineInstance> combines = new List<CombineInstance>(); List<Vector2[]> olduvs = new List<Vector2[]>(); for (int i = 0; i < sk. Length; i ++ ) { CombineInstance combine = new CombineInstance(); combine.mesh = sk[i].sharedMesh; combine.transform = sk[i].transform.localToWorldMatrix; Vector2[] olduv = sk[i].sharedMesh.uv; olduvs.Add(olduv); Vector2[] newuv = new Vector2[olduv. Length]; for (int j = 0; j < olduv. Length; j++ ) { newuv[j] = new Vector2(rects[i].x + rects[i].width * olduv[j].x, rects[i].y + rects[i].height * olduv[j].y) ; } combine.mesh.uv = newuv; combines. Add(combine); } mesh. CombineMeshes(combines. ToArray(), true, true); for (int i = 0; i < sk. Length; i ++ ) { sk[i].sharedMesh.uv = olduvs[i]; } //export grid AssetDatabase.CreateAsset(mesh, "Assets/" + path + ".asset"); //export texture File.WriteAllBytes(Application.dataPath + "/" + path + ".png", meshtexture.EncodeToPNG()); } public void _BakeAnimationClip(float max, float min) { var mesh = new Mesh(); //calculate the difference float vertexDiff = max - min; //Return a number from 0 to 1 through the difference (simple function, pass in float and return float) Func<float, float> cal = (v) => (v - min) / vertexDiff; // vertex count int currentPixelIndex = 0; //loop through all frames of the animation for (int i = 0; i < frameCount; i ++ ) { // Calculate the time of each frame float t = i * 1f / 30; for (int k = 0; k < animator. Length; k ++ ) { //Play the animation at the specified time clip.SampleAnimation(animator[k].gameObject, t); mesh. Clear(false); sk[k].BakeMesh(mesh); var vertices = mesh.vertices; //Debug. Log("vertices:" + vertices. Length); //loop through all vertices of the mesh for (int v = 0; v < vertices. Length; v ++ ) { var vertex = vertices[v]; //Convert the vertex coordinates into a color value from 0 to 1 Color c = new Color(cal(vertex.x), cal(vertex.y), cal(vertex.z)); //One-dimensional to two-dimensional //Calculate the position of the color in the image through the vertex id int x = currentPixelIndex % size; int y = currentPixelIndex / size; //write color to image texture.SetPixel(x, y, c); currentPixelIndex++; } } } //save Picture texture. Apply(); Debug. Log("currentPixelIndex:" + currentPixelIndex); File.WriteAllBytes(Application.dataPath + "/" + path + "Anim.png", texture.EncodeToPNG()); } public void _CalculateVertexMinAndMax(out float vertexMin, out float vertexMax) { //The default is the maximum and minimum values of float float min = float.MaxValue; float max = float. MinValue; Mesh preCalMesh = new Mesh(); for (int f = 0; f < frameCount; f ++ ) { float t = f * 1f / 30; for (int k = 0; k < animator. Length; k ++ ) { //Play the animation at the specified time clip.SampleAnimation(animator[k].gameObject, t); //Get the grid of the current character skin sk[k].BakeMesh(preCalMesh); //loop through all vertices of the mesh for (int v = 0; v < preCalMesh.vertexCount; v ++ ) { var vertex = preCalMesh. vertices[v]; //Take the minimum value of x,y,z //min = Mathf.Floor(Mathf.Min(min, vertex.x, vertex.y, vertex.z)); min = Mathf.Min(min, vertex.x, vertex.y, vertex.z); //Take the maximum value of x,y,z //max = Mathf. Ceil(Mathf. Max(max, vertex. x, vertex. y, vertex. z)); max = Mathf.Max(max, vertex.x, vertex.y, vertex.z); } } } vertexMin = min; vertexMax = max; } }
The shader that displays the animation
Shader "Unlit/MyGPUInstance" { Properties { _MainTex("Texture", 2D) = "white" {} _FaceTex("Face", 2D) = "white" {} _AnimTex("Anim", 2D) = "white" {} _Fmax("fmax",Float) = 87 _VCount("vCount", Float) = 3466 _Size("size",Float) = 1024 _VertexMax("VertexMax",Float) = 2 _VertexMin("VertexMin",Float) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM //Step 1: Add variants to sharder and use shader to support instance #pragma multi_compile_instancing #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(int, _VMax0)//maximum number of vertices UNITY_DEFINE_INSTANCED_PROP(int, _VMin0)//minimum number of vertices UNITY_DEFINE_INSTANCED_PROP(float4, _FaceVec)//The texture coordinates of the face UNITY_INSTANCING_BUFFER_END (Props) struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; // return vertex id uint vid : SV_VertexID; //Step 2: add instanceID to the vertex shader input structure UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float4 faceVec : VECTOR; int clip : INT; //Step 3: add instanceID to the vertex shader output structure UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D_MainTex; sampler2D_FaceTex; sampler2D_AnimTex; uint _Fmax; uint _VCount; float_Size; float _VertexMax; float _VertexMin; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; //Step 4: Related settings of instanceid at the vertex UNITY_SETUP_INSTANCE_ID(v); //Step 5: Pass the instanceid vertex to the fragment UNITY_TRANSFER_INSTANCE_ID(v, o); int vmax = UNITY_ACCESS_INSTANCED_PROP(Props, _VMax0); int vmin = UNITY_ACCESS_INSTANCED_PROP(Props, _VMin0); o.faceVec = UNITY_ACCESS_INSTANCED_PROP(Props, _FaceVec); // current frame number uint f = fmod(ceil(_Time.y * 30), _Fmax); if (v.vid < vmax & & v.vid > vmin) { // current vertex uint index = v.vid + f * _VCount; //Calculate the xy coordinates of the current vertex in the image uint x = index % (uint)_Size; uint y = index / _Size; //Convert xy coordinates to uv coordinates from 0 to 1 float uvx = x / _Size; float uvy = y / _Size; // Get the color of the uv coordinates in the picture and assign it to the vertex coordinates v.vertex = tex2Dlod(_AnimTex, float4(uvx, uvy, 0, 0)) * (_VertexMax - _VertexMin) + _VertexMin; o.clip = 1; } else { o.clip = -1; } o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { //Step 6: Related settings of instanceid in the fragment UNITY_SETUP_INSTANCE_ID(i); // sample the texture fixed4 col = tex2D(_MainTex, i.uv); fixed4 face = tex2D(_FaceTex, float2(i.uv.x * i.faceVec.x + i.faceVec.z,i.uv.y * i.faceVec.y + i.faceVec.w)); col = lerp(col, face, face.a); clip(i. clip); // get the color set by the CPU // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
create code multiple times
using System. Collections; using System.Collections.Generic; using UnityEngine; public class CreatCube : MonoBehaviour { public GameObject cube; public int n = 10; // Start is called before the first frame update void Start() { Init(); } public void Init() { //create prefab for (int x = -n; x < n; x ++ ) { for (int z = -n; z < n; z ++ ) { if (Random. Range(0,10) < 1) { GameObject c = Instantiate(cube, transform); c.transform.position = new Vector3(x, 0, z); MaterialPropertyBlock props = new MaterialPropertyBlock(); switch (Random. Range(0, 4)) { case 1: props.SetInt("_VMax0", 6859); props.SetInt("_VMin0", 3466); props.SetVector("_FaceVec", new Vector4(8, 8, -4.2f, 0)); c.transform.eulerAngles = new Vector3(0, Random.Range(0, 360), 0); break; case 2: props.SetInt("_VMax0", 6859 + 3564); props.SetInt("_VMin0", 6859); props.SetVector("_FaceVec", new Vector4(8, 8, -0.2f, -4)); c.transform.eulerAngles = new Vector3(-90, Random.Range(0, 360), 0); break; case 3: props.SetInt("_VMax0", 13581); props.SetInt("_VMin0", 6859 + 3564); props.SetVector("_FaceVec", new Vector4(8, 8, -4.2f, -4)); c.transform.eulerAngles = new Vector3(-90, Random.Range(0, 360), 0); break; default: props.SetInt("_VMax0", 3466); props.SetInt("_VMin0", 0); props.SetVector("_FaceVec", new Vector4(8, 8, -0.2f, 0)); c.transform.eulerAngles = new Vector3(0, Random.Range(0, 360), 0); break; } c.GetComponent<MeshRenderer>().SetPropertyBlock(props); } } } } }
Part of the data in the code only applies to my model