Unity 材质的冗余贴图引用清除器

Unity 中有 .mat 文件, 一般称之为: 材质球.

本质来说材质球并不是一个资源文件, 而是一堆设置, 里面包含了 Shader 和若干 Texture, 其中 Shader 和 Texture 才是真正的资源文件, 材质球只是将他们组合在一起.

也可以理解为材质球文件将 Shader 进行了图形化展示, 将 Shader 代码中需要的 Texture 和属性数据全部暴露出来, 以便开发者们快速对这些变量赋值.

冗余引用会造成的问题

无论是使用 Resources 还是 AssetBundle, 冗余的贴图引用对资源的打包和加载都没有影响.

但是如果冗余的纹理贴图被作为独立的 AssetBundle 包来处理, 则该纹理贴图会作为依赖项存在.

冗余产生原因: 撤销

Unity 的材质球有一个特性, 那就是对材质球的操作可以 "撤销". 而撤销的实现方式是将之前设置过的贴图引用也会保存在 .mate 文件中, 这样就导致冗余引用.

比如美术人员操作材质球的时候, 先将一张贴图拖进去, 然后发现这里不需要赋值, 于是撤销, 但是即使撤销了, 贴图的引用依然保存在 mate 文件中, 最终打包时这个依赖关系还是会被打包进去.

冗余产生原因: 更换 Shader

材质球的 Shader 是可以更换的, 这样也会导致冗余引用的产生.

比如美术人员制作材质球的时候, 直接拷贝了一份相似的材质球, 这个材质球上引用了 3 张贴图; 之后美术人员更换了一个 Shader, 这个 Shader 只用到了其中的 1 张贴图.

更换之后会发现这张贴图自动使用了之前的贴图引用, 对于美术工作人员是十分方便的, 堪称智能!

但是这个智能是有代价的, 没有使用到的另外 2 张贴图的引用依然被保存在 mate 文件中, 这样就产生了冗余引用.

清除冗余贴图引用的脚本

使用下面的脚本可以清除整个项目中的冗余贴图引用.

使用方法:

  1. 设置工程路径 PRE_PATH 的值, 设置到 Assets 文件夹的上一级.
  2. 在自定义的窗口类的 OnGUI 方法中调用 OnGUI_RedundantTextureReferencesCleaner 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

public class RedundantTextureReferencesCleaner
{
private const float UI_DEFAULT_MARGIN = 5;
private const float UI_INPUT_AREA_WIDTH = 400;
private const float UI_BUTTON_WIDTH = 120;
private const float UI_BUTTON_HEIGHT = 25;

private static bool RedundantTextureReferencesFoldout = true;
private static string RedundantTextureReferencesPath = string.Empty;

public static void OnGUI_RedundantTextureReferencesCleaner()
{
GUILayout.Space(2 * UI_DEFAULT_MARGIN);

RedundantTextureReferencesFoldout = EditorGUILayout.Foldout(RedundantTextureReferencesFoldout, "Detect Unused Material", true);
if (!RedundantTextureReferencesFoldout)
{
return;
}

GUILayout.BeginVertical("Box");
RedundantTextureReferencesPath = EditorGUILayout.TextField("Input Path To Detect", RedundantTextureReferencesPath, GUILayout.Width(UI_INPUT_AREA_WIDTH));
GUILayout.EndVertical();

GUILayout.BeginHorizontal("Box");
if (GUILayout.Button("Detect Shader", GUILayout.Height(UI_BUTTON_HEIGHT), GUILayout.Width(UI_BUTTON_WIDTH)))
{
DetectRedundantTextureReferences();
}

GUILayout.EndHorizontal();
}

private static void DetectRedundantTextureReferences()
{
GetAllFillPath(RedundantTextureReferencesPath, "*.mat", out string message, out string[] allFileName);
if (!string.IsNullOrEmpty(message))
{
Debug.Log(message);
return;
}

Debug.Log($"Find {allFileName.Length} Materials!");
var assetFileName = GetAllFileAssetPath(allFileName);

// 读取预制体
int progressBarCounter = 0;
int repairCounter = 0;
var materials = new List<Material>();
for (int index = 0; index < assetFileName.Count; index++)
{
EditorUtility.DisplayProgressBar("读取材质资源中", $"{index + 1}/{assetFileName.Count}", (float) (index + 1) / assetFileName.Count);
materials.Add(AssetDatabase.LoadAssetAtPath<Material>(assetFileName[index]));
}

EditorUtility.ClearProgressBar();
Debug.Log($"Prefabs: 共读取了 {materials.Count} 个材质");

// 遍历材质
foreach (var material in materials)
{
if (EditorUtility.DisplayCancelableProgressBar("检测中", $"{++progressBarCounter}/{materials.Count}",
(float) progressBarCounter / materials.Count))
{
EditorUtility.ClearProgressBar();
return;
}

if (RepairMaterial(material))
{
repairCounter++;
}
}

Debug.Log($"RepairCounter: 共修复了 {repairCounter} 个问题");
}

private static bool RepairMaterial(Material _material)
{
var isRepaired = false;
var textureGUIDs = CollectTextureGUIDs(_material);
string materialPathName = Path.GetFullPath(AssetDatabase.GetAssetPath(_material));
var strBuilder = new StringBuilder();

using (var reader = new StreamReader(materialPathName))
{
var regex = new Regex(@"\s+guid:\s+(\w+),");
string line = reader.ReadLine();
while (null != line)
{
if (line.Contains("m_Texture:"))
{
// 包含纹理贴图引用的行,使用正则表达式获取纹理贴图的 guid
var match = regex.Match(line);
if (match.Success)
{
string textureGUID = match.Groups[1].Value;
if (textureGUIDs.Contains(textureGUID))
{
strBuilder.AppendLine(line);
}
else
{
// 材质没有用到纹理贴图,guid 赋值为 0 来清除引用关系
isRepaired = true;
strBuilder.AppendLine(line.Substring(0,
line.IndexOf("fileID:", StringComparison.Ordinal) + 7) + " 0}");
}
}
else
{
strBuilder.AppendLine(line);
}
}
else
{
strBuilder.AppendLine(line);
}

line = reader.ReadLine();
}
}

using (var writer = new StreamWriter(materialPathName))
{
writer.Write(strBuilder.ToString());
}

return isRepaired;
}

private static HashSet<string> CollectTextureGUIDs(Material _material)
{
var textureGUIDs = new HashSet<string>();
for (int i = 0; i < ShaderUtil.GetPropertyCount(_material.shader); ++i)
{
if (ShaderUtil.ShaderPropertyType.TexEnv !=
ShaderUtil.GetPropertyType(_material.shader, i))
{
continue;
}

var texture = _material.GetTexture(ShaderUtil.GetPropertyName(_material.shader, i));
if (null == texture)
{
continue;
}

string textureGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(texture));
if (!textureGUIDs.Contains(textureGUID))
{
textureGUIDs.Add(textureGUID);
}
}

return textureGUIDs;
}

private static void GetAllFillPath(string path, string strFilter, out string message, out string[] result)
{
result = null;
message = string.Empty;

string detectPath = Path.Combine(Application.dataPath, path);

if (string.IsNullOrEmpty(detectPath))
{
return;
}

if (!Directory.Exists(detectPath))
{
message = "路径不存在!";
return;
}

result = Directory.GetFiles(detectPath, strFilter, SearchOption.AllDirectories);
}

private static List<string> GetAllFileAssetPath(in string[] paths)
{
var result = new List<string>();

for (int i = 0; i < paths.Length; i++)
{
EditorUtility.DisplayProgressBar("读取资源中", $"{i + 1}/{paths.Length}", (float) (i + 1) / paths.Length);
var path = paths[i].Substring(paths[i].IndexOf("Assets", StringComparison.Ordinal));
if (path.IndexOf(".meta", StringComparison.OrdinalIgnoreCase) < 0)
{
result.Add(path);
}
}

EditorUtility.ClearProgressBar();
return result;
}
}