[性能优化&工具类] 批量Mesh网格压缩

问题描述:

对于3D游戏工程来说,美术资源的存储几乎占据了绝大多数的空间,而对于一个3d 模型文件,MeshFilter(网格过滤器)负责存储物体的网格 以及贴图。依靠MeshRender(网格渲染器)跟据MeshFilter的信息去绘制此物体。Mesh 属性存储众多物理信息,比如顶点的位置、法线、切线。能否在不破坏原有模型外观的情况下尽量减少模型所占体积呢。

解决思路:

这里可以根据模型精度制定压缩规则,使用 Dictionary<VertexAttribute, VertexAttributeFormat> 字典存储(K值对应,顶点依赖属性。V表示它的精度)
比如低模的文件,远景或者边缘物体,小尺寸物体可以不需要添加UV。
根据不同的规则对文件夹下的模型进行分类,依照压缩类型,使用上述字典DescriptorReplaceMap来存储需要替换的顶点属性格式,然后将例如位置、法线和切线的属性格式从Float32或Float16进行替换。这样可以在保持Mesh数据结构不变的情况下,减少存储这些属性所需的内存空间。

压缩过程

根据压缩规则OptimizeType进行分类

OptimizeType 压缩方式

 [SerializeField]
public enum OptimizeType
{
    None,
    CompressByUnityDefault,                     //使用unity默认压缩方式
    CompressAllToFloat16,                       //不建议使用,可以使用Unity自带配置
    CompressPosNormalAndTangentTo8Channel,  //基本等同于CompressAllToFloat16,建议高模
    [Obsolete("法线精度较低,不适用高模、低模")]
    CompressPosNormalAndTangentTo6Channel,  //会较大缺少精度
    CompressPosNormalTo6Channel,  //会删除切线并保留较高精度的法线,建议低模+地形
    CompressNormal,                 //不压缩position, 删除切线并保留较高精度的法线(建议精度较大的不分块地形(与剔除UV联用))
    CompressPosToFloat32,                    //只保留position
    [Obsolete("此类型暂时无法使用")]
    CompressPosNormalTo4Channel,    //将法线压缩到pos的w分量,目前unity的pos.w只能存放符号位
}

填充字典 DescriptorReplaceMap

/// <summary>
/// get optimize mesh
/// worldVertexs : 为空时正常压缩,不为空时将会存储相对坐标
/// </summary>
        public static Mesh GetOptimizeMesh(Mesh originMesh, OptimizeType optimizeType, bool withoutUVs = false, Vector3[] worldVertexs = null)
        {
            if (originMesh == null || originMesh?.tangents.Length == 0)
            {
                Debug.LogWarning("mesh is null or has been optimize");
                return null;
            }
            DescriptorReplaceMap.Clear();
            switch (optimizeType)
            {
                case OptimizeType.None:
                    break;
                case OptimizeType.CompressByUnityDefault:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float32);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.Float16);
                    DescriptorReplaceMap.Add(VertexAttribute.Tangent, VertexAttributeFormat.Float16);
                    break;
                case OptimizeType.CompressAllToFloat16:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float16);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.Float16);
                    DescriptorReplaceMap.Add(VertexAttribute.Tangent, VertexAttributeFormat.Float16);
                    break;
                case OptimizeType.CompressPosNormalAndTangentTo8Channel:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float16);
                    //DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float32);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.Float16);
                    break;
                case OptimizeType.CompressPosNormalAndTangentTo6Channel:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float16);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.UInt16);
                    break;
                case OptimizeType.CompressPosNormalTo6Channel:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float16);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.Float16);
                    break;
                case OptimizeType.CompressNormal:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float32);
                    DescriptorReplaceMap.Add(VertexAttribute.Normal, VertexAttributeFormat.Float16);
                    break;
                case OptimizeType.CompressPosToFloat32:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float32);
                    break;
                case OptimizeType.CompressPosNormalTo4Channel:
                    DescriptorReplaceMap.Add(VertexAttribute.Position, VertexAttributeFormat.Float16);
                    break;
                default:
                    return null;
            }

            if (!withoutUVs)
            {
                DescriptorReplaceMap.Add(VertexAttribute.TexCoord0, VertexAttributeFormat.Float16);
                DescriptorReplaceMap.Add(VertexAttribute.TexCoord1, VertexAttributeFormat.Float16);
                DescriptorReplaceMap.Add(VertexAttribute.TexCoord2, VertexAttributeFormat.Float16);
                DescriptorReplaceMap.Add(VertexAttribute.TexCoord3, VertexAttributeFormat.Float16);
            }
			DescriptorReplaceMap.Add(VertexAttribute.BlendWeight, VertexAttributeFormat.Float16);
            DescriptorReplaceMap.Add(VertexAttribute.BlendIndices, VertexAttributeFormat.UInt16);

            //-------------压缩开始-------------------
            var newMesh = MeshOptimize(originMesh, optimizeType, worldVertexs);
#if UNITY_2020_2_OR_NEWER
            for (int i = 0; i < 2; i++)
            {
                newMesh.RecalculateUVDistributionMetrics(i);
            }
#else
            float metrics0 = originMesh.GetUVDistributionMetric(0);
            float metrics1 = originMesh.GetUVDistributionMetric(1);
            SerializedObject new_serializedObject = new SerializedObject(newMesh);
            var new_metrics0 = new_serializedObject.FindProperty("m_MeshMetrics[0]");
            var new_metrics1 = new_serializedObject.FindProperty("m_MeshMetrics[1]");
            if (new_metrics0 != null) new_metrics0.floatValue = metrics0;
            if (new_metrics1 != null) new_metrics1.floatValue = metrics1;
            new_serializedObject.ApplyModifiedPropertiesWithoutUndo();
#endif
            newMesh.name = originMesh.name;
            newMesh.UploadMeshData(true);
            return newMesh;
        }

根据分类,降低精度

处理顶点属性描述符(VertexAttributeDescriptor)
根据map中对应的优化类型(optimizeType)和属性类型(attributeDesc[i].attribute),对temp的格式(format)和维度(dimension)进行调整。最后,将调整后的temp添加到优化后的描述符列表(optimizeDesc)中。

Mesh MeshOptimize(Mesh originMesh, OptimizeType optimizeType, Vector3[] worldVertexs = null)
{

    //-----
    
                VertexAttributeFormat optimizeFormat = attributeDesc[i].format;
                if (DescriptorReplaceMap.TryGetValue(attributeDesc[i].attribute, out optimizeFormat))
                {
                    VertexAttributeDescriptor temp = new VertexAttributeDescriptor();
                    temp = attributeDesc[i];

                    temp.format = optimizeFormat;
                    if (isFormat_16(temp.format))
                        temp.dimension += temp.dimension % 2;

                    if (optimizeType == OptimizeType.CompressPosNormalAndTangentTo8Channel && attributeDesc[i].attribute == VertexAttribute.Position)
                    {
                        temp.dimension = 4;
                    }

                    if ((optimizeType == OptimizeType.CompressPosNormalAndTangentTo6Channel || optimizeType == OptimizeType.CompressPosNormalTo6Channel || optimizeType == OptimizeType.CompressNormal) && temp.attribute == VertexAttribute.Normal)
                    {
                        temp.dimension = 2;
                    }

                    optimizeDesc.Add(temp);
                }
            }

    //------

    //收集顶点数据加入缓存区

    //构造新Mesh,返回Mesh
}

收集替换后的Mesh顶点数据

public static void CollectionData(VertexAttributeDescriptor[] optimizeDesc, Mesh originMesh, OptimizeType optimizeType, Vector3[] worldVertexs = null)
{
  VertexAttributeDescriptor[] attributeDesc = originMesh.GetVertexAttributes();
        int maxStream = 0;
        for (int i = 0; i < optimizeDesc.Length; i++)
        {
            if (maxStream < optimizeDesc[i].stream)
            {
                maxStream = optimizeDesc[i].stream;
            }
        }
    for (int streamIndex = 0; streamIndex <= maxStream; streamIndex++)
            {
                for (int vertexIndex = 0; vertexIndex < originMesh.vertexCount; vertexIndex++)
                {
                    for (int i = 0; i < optimizeDesc.Length; i++)
                    {
                        //规则1
                        if (optimizeDesc[i].attribute == VertexAttribute.Position && optimizeDesc[i].stream == streamIndex)
                        {
                            if (optimizeType != OptimizeType.CompressPosNormalTo4Channel)
                            {
                                if (optimizeType == OptimizeType.CompressPosNormalAndTangentTo6Channel ||
                                    optimizeType == OptimizeType.CompressPosNormalAndTangentTo8Channel)
                                {
                                    if (worldVertexs != null)
                                    {
                                        DataOptimize(new Vector4(pos[vertexIndex].x- worldVertexs[vertexIndex].x, pos[vertexIndex].y- worldVertexs[vertexIndex].y, pos[vertexIndex].z- worldVertexs[vertexIndex].z, tangent[vertexIndex].w), GetFormat(attributeDesc, optimizeDesc[i].attribute), optimizeDesc[i].format, optimizeDesc[i].dimension);
                                    }
                                    else
                                    {
                                        DataOptimize(new Vector4(pos[vertexIndex].x, pos[vertexIndex].y, pos[vertexIndex].z, tangent[vertexIndex].w), GetFormat(attributeDesc, optimizeDesc[i].attribute), optimizeDesc[i].format, optimizeDesc[i].dimension);
                                    }
                                }
                                else
                                    DataOptimize(pos[vertexIndex], GetFormat(attributeDesc, optimizeDesc[i].attribute), optimizeDesc[i].format, optimizeDesc[i].dimension);
                            }
                            else
                                DataOptimizeNormalToPosW(pos[vertexIndex], CompressNormalize(normal[vertexIndex]), GetFormat(attributeDesc, optimizeDesc[i].attribute), optimizeDesc[i].format, optimizeDesc[i].dimension);
                        }
                        //其他规则等..

                    }
                }
                        streamOffset.Add(vertexBuffer.Count);//更新顶点缓存区数量

}

//数据格式化
 private static void DataOptimize(int4 data, VertexAttributeFormat formatsrc, VertexAttributeFormat formatdst, int dimension)
        {
            if (formatdst == VertexAttributeFormat.UInt16)
                vertexBuffer.AddRange(Int16_4ToBytes(data));
            else if (formatdst == VertexAttributeFormat.SInt16)
                vertexBuffer.AddRange(Int16_4ToBytes(data));
            else if (formatdst == VertexAttributeFormat.UInt32)
                vertexBuffer.AddRange(Int32_4ToBytes(data));
            else if (formatdst == VertexAttributeFormat.SInt32)
                vertexBuffer.AddRange(Int32_4ToBytes(data));
        }

构造新Mesh

   Mesh newMesh = new Mesh();
    //newMesh.indexFormat = IndexFormat.UInt32;
    int vertexCount = originMesh.vertexCount;

    newMesh.SetVertexBufferParams(vertexCount, optimizeDesc.ToArray());

    newMesh.SetVertexBufferData(vertexBuffer.ToArray(), 0, 0, streamOffset[0], 0);
    for (int streamIndex = 0; streamIndex < streamOffset.Count - 1; streamIndex++)
    {
        newMesh.SetVertexBufferData(vertexBuffer.ToArray(), streamOffset[streamIndex], 0, streamOffset[streamIndex + 1] - streamOffset[streamIndex], streamIndex + 1);
    }

    int subMeshCount = originMesh.subMeshCount;
    newMesh.subMeshCount = subMeshCount;
    for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
    {
        newMesh.SetIndices(originMesh.GetIndices(subMeshIndex), originMesh.GetTopology(subMeshIndex), subMeshIndex);
    }

    newMesh.colors = originMesh.colors;
    newMesh.bindposes = originMesh.bindposes;
    newMesh.OptimizeReorderVertexBuffer();
    //Bound
    newMesh.bounds = originMesh.bounds;
    for (int i = 0; i < originMesh.subMeshCount; i++)
    {
        var subMesh = newMesh.GetSubMesh(i);
        subMesh.bounds = originMesh.GetSubMesh(i).bounds;
        subMesh.topology = originMesh.GetSubMesh(i).topology;
        newMesh.SetSubMesh(i, subMesh, MeshUpdateFlags.DontRecalculateBounds);
    }
    return newMesh;

在外部更换为新的Mesh网格

           var newMesh = MeshOptimize(originMesh, optimizeType, worldVertexs);
#if UNITY_2020_2_OR_NEWER
            for (int i = 0; i < 2; i++)
            {
                //将网格的UV分布指标从顶点和uv坐标重新计算。
                newMesh.RecalculateUVDistributionMetrics(i);
            }
#else
            float metrics0 = originMesh.GetUVDistributionMetric(0);
            float metrics1 = originMesh.GetUVDistributionMetric(1);
            SerializedObject new_serializedObject = new SerializedObject(newMesh);
            var new_metrics0 = new_serializedObject.FindProperty("m_MeshMetrics[0]");
            var new_metrics1 = new_serializedObject.FindProperty("m_MeshMetrics[1]");
            if (new_metrics0 != null) new_metrics0.floatValue = metrics0;
            if (new_metrics1 != null) new_metrics1.floatValue = metrics1;
            new_serializedObject.ApplyModifiedPropertiesWithoutUndo();
#endif
            newMesh.name = originMesh.name;
            newMesh.UploadMeshData(true);
            return newMesh;

处理特殊问题

对应部分fbx文件,压缩后会出现光照贴图异常变暗的情况。
对于这类物体,经过调查发现是其prefab的子级中也包含meshFilter。比起整体包含一个meshfilter的情况,相互之间的渲染收到了影响。
把出问题的bundle文件的mesh回滚,即用原来精度的mesh.
4.gif

递归回退

    void RevertItemSelfAndChildren(GameObject obj)
    {
      try
      {
        if (obj != null)
        {
            // 如果当前物体有MeshFilter组件,则更换其sharedMesh
            string curObjName = obj.name;
            if (curObjName.EndsWith("(Clone)"))
            {
                curObjName = curObjName.Substring(0, curObjName.Length - 7);
            }
            
            if (fbxMeshMap.ContainsKey(curObjName))
            {
                MeshContent originMeshContent = fbxMeshMap[curObjName];
                MeshFilter meshFilter = obj.GetComponent<MeshFilter>();
                MeshRenderer meshRenderer = obj.GetComponent<MeshRenderer>();
                if (meshFilter != null && originMeshContent.curMesh != null)
                {
                    Debug.Log(obj.name + "的sharedMesh:" + meshFilter.sharedMesh.name + "替换为" + originMeshContent.curMesh);
                    //在字典中找到对应fbx,提取Mesh。给prefab重新更换
                    meshFilter.sharedMesh = originMeshContent.curMesh;
                    meshFilter.sharedMesh.RecalculateBounds();
                }

                if (originMeshContent.name != null && meshRenderer != null)
                {
                    if ( originMeshContent.curMaterials!= null && meshRenderer.sharedMaterial!= null)
                    {
                        Debug.Log(obj.name + "的sharedMaterials:" + meshRenderer.sharedMaterial.name + "替换为" + originMeshContent.curMaterials);
                        meshRenderer.sharedMaterial= originMeshContent.curMaterials;
                    }
                }
                EditorUtility.SetDirty(obj);
                string sourcePath = AssetDatabase.GetAssetPath(obj);
                PrefabUtility.SaveAsPrefabAsset(obj, sourcePath);
            }
            
            if ( obj != null && obj.transform.childCount > 0)
            {
                // 遍历所有子节点并更换它们的MeshFilter
                foreach (Transform child in obj.transform)
                {
                    RevertItemSelfAndChildren(child.gameObject);
                }    
            }
        }
      }
      catch (Exception e)
      {
          Console.WriteLine(e);
          throw;
      }
       
    }

编辑器扩展

Editor目录下封装为工具统一使用,继承EditorWindow 制作为编辑器窗口
image.png
image.png
image.png
image.png

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++性能优化检测工具可以帮助开发人员分析和优化他们的代码,以提高程序的性能。以下是一些常用的C++性能优化检测工具: 1. Profilers(性能分析器):性能分析器可以帮助开发人员找到程序中的性能瓶颈。它们可以测量函数调用的时间、内存使用情况和其他指标,并生成性能报告。一些常用的性能分析器包括GNU gprof、Valgrind和Intel VTune。 2. 编译器优化选项:大多数C++编译器都提供了一些优化选项,可以在编译时对代码进行优化。例如,GCC编译器提供了一系列的优化选项,如-O1、-O2和-O3,可以根据需求选择不同级别的优化。 3. 静态代码分析工具:静态代码分析工具可以检测代码中的潜在问题和性能瓶颈。它们可以帮助开发人员找到未使用的变量、内存泄漏和其他常见的错误。一些常用的静态代码分析工具包括Cppcheck和Clang Static Analyzer。 4. 动态内存分析工具:动态内存分析工具可以帮助开发人员检测内存泄漏和内存访问错误。它们可以跟踪程序运行时的内存分配和释放,并生成报告。一些常用的动态内存分析工具包括Valgrind和Dr. Memory。 5. 可视化性能分析工具:可视化性能分析工具可以以图形化的方式展示程序的性能数据,帮助开发人员更直观地理解和优化代码。一些常用的可视化性能分析工具包括Intel VTune和Google Performance Tools。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值