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