[性能优化工具类] 批量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_NEWERfor (int i = 0; i < 2; i++){newMesh.RecalculateUVDistributionMetrics(i);}
#elsefloat 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();
#endifnewMesh.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++){//规则1if (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);}}elseDataOptimize(pos[vertexIndex], GetFormat(attributeDesc, optimizeDesc[i].attribute), optimizeDesc[i].format, optimizeDesc[i].dimension);}elseDataOptimizeNormalToPosW(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();//BoundnewMesh.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_NEWERfor (int i = 0; i < 2; i++){//将网格的UV分布指标从顶点和uv坐标重新计算。newMesh.RecalculateUVDistributionMetrics(i);}
#elsefloat 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();
#endifnewMesh.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组件,则更换其sharedMeshstring 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){// 遍历所有子节点并更换它们的MeshFilterforeach (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

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

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

相关文章

使用pandas的merge()和join()函数进行数据处理

目录 一、引言 二、pandas的merge()函数 基本用法 实战案例 三、pandas的join()函数 基本用法 实战案例 四、merge()与join()的比较与选择 使用场景&#xff1a; 灵活性&#xff1a; 选择建议&#xff1a; 五、进阶案例与代码 六、总结 一、引言 在数据分析和处理…

stripe支付

使用第一个示例 1、示例中的PRICE_ID需要去Stripe控制台->产品目录创建产品 1、 添加产品 2、点击查看创建的产品详情 4、这个API ID就是demo中的PRICE_ID 注意&#xff1a;需要注意的是&#xff0c;测试模式和生产模式中的 $stripeSecretKey 需要对应上。简而言之就是不能生…

AI实景自动无人直播软件:引领直播行业智能化革命;提升直播效果,无人直播软件助力智能讲解

随着科技的快速发展&#xff0c;AI实景自动无人直播软件正在引领直播行业迈向智能化革命。它通过智能讲解、一键开播和智能回复等功能&#xff0c;为商家提供了更高效、便捷的直播体验。此外&#xff0c;软件还支持手机拍摄真实场景或搭建虚拟场景&#xff0c;使直播画面更好看…

如何将数据导入python

Python导入数据的三种方式&#xff1a; 1、通过标准的Python库导入CSV文件 Python提供了一个标准的类库CSV文件。这个类库中的reader()函数用来导入CSV文件。当CSV文件被读入后&#xff0c;可以利用这些数据生成一个NumPy数组&#xff0c;用来训练算法模型。 from csv import…

如何使用dockerfile文件将项目打包成镜像

要根据Dockerfile文件来打包一个Docker镜像&#xff0c;你需要遵循以下步骤。这里假设你已经安装了Docker环境。 1. 准备Dockerfile 确保你的Dockerfile文件已经准备就绪&#xff0c;并且位于你希望构建上下文的目录中。Dockerfile是一个文本文件&#xff0c;包含了用户可以调…

软件系统工程建设全套资料(交付清单)

软件全套精华资料包清单部分文件列表&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&#xff0c;需求调研计划&#xff0c;用户需求调查单&#xff0c;用户需求说明书&#xff0c;概要设计说明书&#xff0c…

RTSP/Onvif安防监控系统EasyNVR级联视频上云系统EasyNVS报错“Login error”的原因排查与解决

EasyNVR安防视频云平台是旭帆科技TSINGSEE青犀旗下支持RTSP/Onvif协议接入的安防监控流媒体视频云平台。平台具备视频实时监控直播、云端录像、云存储、录像检索与回看、告警等视频能力&#xff0c;能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、W…

多行字符串水平相加

题目来源与2023河南省ccpc ls [ ........ ........ .0000000 .0.....0 .0.....0 .0.....0 .0.....0 .0.....0 .0000000 ........ , ........ ........ .......1 .......1 .......1 .......1 .......1 .......1 .......1 ........, ......... ......... .2222222. .......2. .…

扩展学习|一文读懂知识图谱

一、知识图谱的技术实现流程及相关应用 文献来源&#xff1a;曹倩,赵一鸣.知识图谱的技术实现流程及相关应用[J].情报理论与实践,2015, 38(12):127-132. &#xff08;一&#xff09;知识图谱的特征及功能 知识图谱是为了适应新的网络信息环境而产生的一种语义知识组织和服务的方…

什么是SSL?SSL安全证书一定要有吗?

什么是SSL证书&#xff1f; SSL证书是数字证书的一种&#xff0c;类似于驾驶证、护照和营业执照的电子副本。因为配置在服务器上&#xff0c;也称为SSL服务器证书。SSL 证书就是遵守 SSL协议&#xff0c;由受信任的数字证书颁发机构CA&#xff0c;在验证服务器身份后颁发&…

基于POSIX标准库的读者-写者问题的简单实现

文章目录 实验要求分析保证读写、写写互斥保证多个读者同时进行读操作 读者优先实例代码分析 写者优先示例代码分析 实验要求 创建一个控制台进程&#xff0c;此进程包含n个线程。用这n个线程来表示n个读者或写者。每个线程按相应测试数据文件的要求进行读写操作。用信号量机制…

AI模型:windows本地运行下载安装ollama运行Google CodeGemma【自留记录】

AI模型&#xff1a;windows本地运行下载安装ollama运行Google CodeGemma【自留记录】 1、下载&#xff1a; 官网下载&#xff1a;https://ollama.com/download&#xff0c;很慢&#xff0c;原因不解释。 阿里云盘下载&#xff1a;https://www.alipan.com/s/jiwVVjc7eYb 提取码…

工业级POE交换机的POE供电功能有哪些好处

工业级POE交换机的POE供电功能是一种高效、方便、安全的供电方式。POE技术能够通过Ethernet网线传输电力和数据&#xff0c;无需额外的电源线路&#xff0c;从而简化了设备的安装和布线工作。在工业环境中&#xff0c;特别是一些远距离、高墙壁或者天花板安装位置不便的地方&am…

聚苯胺纳米纤维膜的制备过程

聚苯胺纳米纤维膜是一种由聚苯胺&#xff08;PANI&#xff09;纳米纤维构成的薄膜材料。聚苯胺是一种具有优良导电性、氧化还原性和化学稳定性的高分子材料&#xff0c;因此聚苯胺纳米纤维膜也具备这些特性&#xff0c;并展现出广阔的应用前景。 在制备聚苯胺纳米纤维膜时&…

RLC防孤岛负载测试的案例和实际应用经验有哪些?

RLC防孤岛负载测试是用于检测并防止电力系统出现孤岛现象的测试方法&#xff0c;孤岛现象是指当电网因故障或停电而与主电网断开连接时&#xff0c;一部分电网仍然与主电网保持连接&#xff0c;形成一个孤立的电网。这种情况下&#xff0c;如果电力系统不能及时检测到孤岛并采取…

Pascal Content数据集

如果您想使用Pascal Context数据集&#xff0c;请安装Detail&#xff0c;然后运行以下命令将注释转换为正确的格式。 1.安装Detail 进入项目终端 #即 这是在我自己的项目下直接进行克隆操作&#xff1a; git clone https://github.com/zhanghang1989/detail-api.git $PASCAL…

一、vue3专栏项目 -- 1、项目介绍以及准备工作

这是vue3TS的项目&#xff0c;是一个类似知乎的网站&#xff0c;可以展示专栏和文章的详情&#xff0c;可以登录、注册用户&#xff0c;可以创建、删除、修改文章&#xff0c;可以上传图片等等。 这个项目全部采用Composition API 编写&#xff0c;并且使用了TypeScript&#…

4G工业路由器快递柜应用案例(覆盖所有场景)

快递柜展示图 随着电商的蓬勃发展,快递行业迎来高速增长。为提高快递效率、保障快件安全,智能快递柜应运而生。但由于快递柜部署环境复杂多样,网络接入成为一大难题。传统有线宽带难以覆盖所有场景,而公用WiFi不稳定且存在安全隐患。 星创易联科技有限公司针对这一痛点,推出了…

视频断点上传

什么是断点续传 通常视频文件都比较大&#xff0c;所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制&#xff0c;但是客户的网络环境质量、电脑硬件环境等参差不齐&#xff0c;如果一个大文件快上传完了网断了没有上传完成&#xf…

Docker安装部署一本通:从Linux到Windows,全面覆盖!(网络资源精选)

文章目录 📖 介绍 📖🏡 说明 🏡⚓️ 相关链接 ⚓️📖 介绍 📖 随着容器技术的飞速发展,Docker已成为现代软件开发和运维不可或缺的工具。然而,不同平台下的Docker安装部署方式各异,这常常让初学者感到困惑。本文将为您详细梳理各平台下Docker的安装部署方法,帮…