问题描述:
对于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.
递归回退
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 制作为编辑器窗口