GPUInstance merges characters to bake vertex animation

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