GPUInstance合并人物烘培顶点动画

该代码示例展示了如何在Unity中利用GPU实例化(GPUInstance)进行批量绘制,通过烘培蒙皮动画为顶点动画来优化性能。文章提供了从动画提取、网格合并到创建自定义Shader显示动画的完整过程。
摘要由CSDN通过智能技术生成

将4个小人合成一个,使用gpu Instance批量绘制可以使用gpu Instance的特性降低批处理,但是gpu Instance只支持mesh filter所有需要将蒙皮动画烘培成顶点动画,通过MaterialPropertyBlock给shader传值区分显示的人物。

烘培人物和动画代码

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("提取"))
        {
            myScript.SaveAsset();
        }
    }
}

public class BakingAnimMesh : MonoBehaviour
{
    public AnimationClip clip;//指定要烘培的动画
    public Animator[] animator;//有动画组件的人物
    public SkinnedMeshRenderer[] sk;//动画人物的蒙皮网格渲染器
    int pnum;//总像素点
    Texture2D texture;
    int size = 0;//图片的宽高
    int vertexCount = 0;//总顶点数
    int frameCount;//总帧数
    public string path = "mini";//导出名称

    //导出动画图片,合并后的网格,合并后的贴图
    public void SaveAsset()
    {
        //总帧数 = 动画时长 * 帧率
        frameCount = Mathf.CeilToInt(clip.length * 30);
        Debug.Log("frameCount:" + frameCount);
        //顶点个数
        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);
        //总像素点 = 总顶点数 * 总帧数
        pnum = vertexCount * frameCount;
        Debug.Log("pnum:" + pnum);
        //将像素点数转换成2的倍数的贴图宽高
        size = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(pnum)));
        Debug.Log("size:" + size);
        texture = new Texture2D(size, size, TextureFormat.RGBAFloat, false, true);
        //计算顶点的最大最小值
        _CalculateVertexMinAndMax(out float min, out float max);
        Debug.Log("min:" + min);
        Debug.Log("max:" + max);
        //烘培图片
        _BakeAnimationClip(max, min);
        //合并导出网格
        Mesh mesh = new Mesh();
        //合并网格贴图
        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();
        //合并网格
        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];
        }
        //导出网格
        AssetDatabase.CreateAsset(mesh, "Assets/" + path + ".asset");
        //导出贴图
        File.WriteAllBytes(Application.dataPath + "/" + path + ".png", meshtexture.EncodeToPNG());
    }

    public  void _BakeAnimationClip(float max, float min)
    {
        var mesh = new Mesh();
        //计算差值
        float vertexDiff = max - min;
        //通过差值返回0到1的一个数(简易函数,传入float返回float)
        Func<float, float> cal = (v) => (v - min) / vertexDiff;
        //顶点计数
        int currentPixelIndex = 0;
        //循环动画的所有帧
        for (int i = 0; i < frameCount; i++)
        {
            //计算每帧的时间
            float t = i * 1f / 30;
            for (int k = 0; k < animator.Length; k++)
            {
                //播放指定时间的动画
                clip.SampleAnimation(animator[k].gameObject, t);
                mesh.Clear(false);
                sk[k].BakeMesh(mesh);

                var vertices = mesh.vertices;
                //Debug.Log("vertices:" + vertices.Length);
                //循环网格的所有顶点
                for (int v = 0; v < vertices.Length; v++)
                {
                    var vertex = vertices[v];
                    //把顶点坐标转换成0到1的颜色值
                    Color c = new Color(cal(vertex.x), cal(vertex.y), cal(vertex.z));
                    //一维转二维
                    //通过顶点id计算该颜色在图片中的位置
                    int x = currentPixelIndex % size;
                    int y = currentPixelIndex / size;
                    //把颜色写入图片
                    texture.SetPixel(x, y, c);
                    currentPixelIndex++;
                }
            }
        }
        //保存图片
        texture.Apply();
        Debug.Log("currentPixelIndex:" + currentPixelIndex);
        File.WriteAllBytes(Application.dataPath + "/" + path + "Anim.png", texture.EncodeToPNG());
    }

    public void _CalculateVertexMinAndMax(out float vertexMin, out float vertexMax)
    {
        //默认为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++)
            {
                //播放指定时间的动画
                clip.SampleAnimation(animator[k].gameObject, t);
                //取出当前人物蒙皮的网格
                sk[k].BakeMesh(preCalMesh);
                //循环网格的所有顶点
                for (int v = 0; v < preCalMesh.vertexCount; v++)
                {
                    var vertex = preCalMesh.vertices[v];
                    //取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);
                    //取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;
    }
}

显示动画的shader

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
            //第一步: sharder 增加变体使用shader可以支持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)//顶点最大数
                UNITY_DEFINE_INSTANCED_PROP(int, _VMin0)//顶点最小数
                UNITY_DEFINE_INSTANCED_PROP(float4, _FaceVec)//脸的贴图坐标
            UNITY_INSTANCING_BUFFER_END(Props)

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                //返回顶点id
                uint vid : SV_VertexID;
                //第二步:instancID 加入顶点着色器输入结构 
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 faceVec : VECTOR;
                int clip : INT;
                //第三步:instancID 加入顶点着色器输出结构 
                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;
                //第四步:instanceid在顶点的相关设置  
                UNITY_SETUP_INSTANCE_ID(v);
                //第五步:传递 instanceid 顶点到片元
                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);
                //当前帧数
                uint f = fmod(ceil(_Time.y * 30), _Fmax);
                if (v.vid < vmax && v.vid > vmin)
                {
                    //当前顶点
                    uint index = v.vid + f * _VCount;
                    //计算当前顶点在图片中的xy坐标
                    uint x = index % (uint)_Size;
                    uint y = index / _Size;
                    //把xy坐标转换为0到1的uv坐标
                    float uvx = x / _Size;
                    float uvy = y / _Size;
                    //获取图片中uv坐标的颜色,赋值给顶点坐标
                    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
            { 
                //第六步:instanceid在片元的相关设置
                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);
                //得到由CPU设置的颜色
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

多次创建代码

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()
    {
        //创建预制体
        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);
                }
            }
        }
    }
}

代码中的部分数据只适用我的模型

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值