FishPlayer

一个喜欢摸鱼的废物

0%

TextMeshPro 图标图集整合小工具

交互希望能在界面上比较方便的显示 🍌*5 这样的效果。好在当前的项目使用TMP,只需要把图标打成一个资源赋给TMP文本组件,就可以在文本中显示图标。

为了让交互策划能够自行调整图标使用的范围以及质量,我随手搓了一个小配置来做到这个事情。
当前我们的美术资源还没有做规范整理,所以项目中存在着尺寸不一致的图标,但对于文本上使用的图标来说,尺寸大概是固定的,所以图集里的图标尺寸也应该要固定下来。
最最开始的时候想的是直接写一个双线性采样线跑跑,做个可以用的程度,但是不是因为我对需求的描述不太好,问Copilot并没有给出我要的做法,我又懒得自己写(其实是加班加傻了,用Texture2D.GetPixelBilinear()可以),于是又开谷歌搜一下,很快就在论坛里找到一个比较有趣的做法。

这个思路是这样的:
1 以我们缩放的目标尺寸创建RT,作为渲染目标
2 把需要缩放的贴图用这个RT渲染一次
3 新的贴图直接从渲染结果中取色
4 非常方便,而且渲染时可以指定贴图filter模式,便于得到质量OK的图图

说实在的这次的笔记没什么含量,只是觉得自己对图形相关的知识太过于欠缺了,无论是理论知识还是API的了解。拼图写功能好几年了,对自己还是挺失望的,好在每次谷歌都能救我(
最近也是一直加班做系统功能,老实说有点心累。手头其实有很多坑没有填,觉得可能是时候停下来了。对工作相关的东西,以及生活技能,做梳理审视,想一下今后的方向和目标。

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
namespace UI.Editor
{
/// <summary> A shortcut to creatre TMP sprite asset </summary>
public class TextMeshProIconPacker : ScriptableObject
{
[Header("Result Config")]
[SerializeField,]
private string _textureOutPutName = "TempIconAtlas";
[SerializeField,]
private string m_spawnedAtlasPath;
[SerializeField,]
private TMP_SpriteAsset m_target;

[Header("Sprite Pack")]
[SerializeField]
private Sprite[] _spriteToPack;
[SerializeField]
private int _maxSize = 512;
[SerializeField]
private int _iconSize = 64;
[SerializeField]
private int _padding = 2;
[SerializeField]
private float _iconScale = 1.2f;

[SerializeField]
private SpriteRect[] m_spriteRects;

[MenuItem("Assets/UI/CreateTMPSpriteConfigTemplate")]
private static void InternalCreateTestAsset()
{
TextMeshProIconPacker asset = ScriptableObject.CreateInstance<TextMeshProIconPacker>();
// TODO Change this path
string assetPath = "Assets/TMPSpriteConfig/TMPSpriteConfigTemplate.asset";
UnityEditor.AssetDatabase.CreateAsset(asset, assetPath);
UnityEditor.AssetDatabase.SaveAssets();
}

[ContextMenu("Create Sprite Atlas")]
public void PackSpritesForTMP_EditorOnly()
{
if (null == _spriteToPack || 0 == _spriteToPack.Length)
{
return;
}
// grab all sprite as textures
Texture2D[] sourceTextures = GatherTextures();
FixImportSettings(sourceTextures, true);
Texture2D[] texture2Pack = GetAdjustedT2D(sourceTextures);

// create sprite rects for atlas
Texture2D texture = new Texture2D(_maxSize, _maxSize, TextureFormat.RGBA32, false);
Rect[] rects = texture.PackTextures(texture2Pack, _padding, _maxSize);
m_spriteRects = new SpriteRect[rects.Length];
for (int i = 0; i < rects.Length; i++)
{
m_spriteRects[i] = new SpriteRect()
{
name = texture2Pack[i].name,
spriteID = GUID.Generate(),
rect = ConvertToPixelRect(texture.width, texture.height, rects[i]),
pivot = 0.5f * Vector2.one,
alignment = SpriteAlignment.Center,
};
}

EditorUtility.SetDirty(this);
AssetDatabase.SaveAssetIfDirty(this);

string path = AssetDatabase.GetAssetPath(this);
string folderPath = Path.GetDirectoryName(path);
string pngPath = $"{folderPath}/{_textureOutPutName}Atlas.png";

// save sprite atlas
Texture2D decopmpresseTex = DeCompress(texture);
byte[] bytes = decopmpresseTex.EncodeToPNG();
File.WriteAllBytes(pngPath, bytes);
int temp = name.Length + 6;
m_spawnedAtlasPath = path.Substring(0, path.Length - temp) + $"{_textureOutPutName}Atlas.png";

// revert sprite readable
FixImportSettings(sourceTextures, false);
for (int i = 0, length = texture2Pack.Length; i < length; i++)
{
Texture.DestroyImmediate(texture2Pack[i]);
}
texture2Pack = null;
AssetDatabase.Refresh();

TextureImporter textureImporter = AssetImporter.GetAtPath(pngPath) as TextureImporter;
textureImporter.textureType = TextureImporterType.Sprite;
textureImporter.spriteImportMode = SpriteImportMode.Multiple;
textureImporter.SaveAndReimport();

textureImporter = AssetImporter.GetAtPath(pngPath) as TextureImporter;
SpriteDataProviderFactories factory = new();
factory.Init();
ISpriteEditorDataProvider dataProvider = factory.GetSpriteEditorDataProviderFromObject(textureImporter);
dataProvider.InitSpriteEditorDataProvider();
dataProvider.SetSpriteRects(m_spriteRects);

// Note: This section is only for Unity 2021.2 and newer
// Register the new Sprite Rect's name and GUID with the ISpriteNameFileIdDataProvider
ISpriteNameFileIdDataProvider spriteNameFileIdDataProvider = dataProvider.GetDataProvider<ISpriteNameFileIdDataProvider>();
List<SpriteNameFileIdPair> nameFileIdPairs = spriteNameFileIdDataProvider.GetNameFileIdPairs().ToList();
nameFileIdPairs.AddRange(m_spriteRects.Select(spriteRect => new SpriteNameFileIdPair(spriteRect.name, spriteRect.spriteID)));
spriteNameFileIdDataProvider.SetNameFileIdPairs(nameFileIdPairs);

dataProvider.Apply();
textureImporter.SaveAndReimport();
}

[ContextMenu("Generate asset by sprite atlas")]
public void ApplyAtlasToAsset()
{
string path = AssetDatabase.GetAssetPath(this);
string folderPath = Path.GetDirectoryName(path);
string pngPath = $"{folderPath}/{_textureOutPutName}Atlas.png";

if (null == m_target)
{
m_target = ScriptableObject.CreateInstance<TMP_SpriteAsset>();
m_target.name = _textureOutPutName;
m_target.spriteInfoList = new();
AssetDatabase.CreateAsset(m_target, $"{folderPath}/{_textureOutPutName}.asset");
}
else
{
// seens like we dun need to destroy it
//Material matToDestroy = m_target.material;
//m_target.material = null;
//Material.DestroyImmediate(matToDestroy);
}
m_target.spriteSheet = AssetDatabase.LoadAssetAtPath<Texture2D>(pngPath);
AddDefaultMaterial(m_target);

Type hackType = typeof(TMP_SpriteAssetMenu);
MethodInfo[] hackMethods = hackType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static);
for (int i = 0, length = hackMethods.Length; i < length; i++)
{
MethodInfo hackMethod = hackMethods[i];
ParameterInfo[] paramInfos = hackMethod.GetParameters();
if (1 == paramInfos.Length && typeof(TMP_SpriteAsset) == paramInfos[0].ParameterType)
{
hackMethod.Invoke(null, new object[] { m_target });
break;
}
}

List<TMP_SpriteCharacter> characters = m_target.spriteCharacterTable;
for (int i = 0, length = characters.Count; i < length; i++)
{
TMP_SpriteCharacter character = characters[i];
character.scale = _iconScale;
}

EditorUtility.SetDirty(m_target);
AssetDatabase.SaveAssetIfDirty(m_target);
}

[ContextMenu("Clear result")]
public void Clear()
{
if (null != m_target)
{
string taragetath = AssetDatabase.GetAssetPath(m_target);
AssetDatabase.DeleteAsset(taragetath);
}
if (!string.IsNullOrEmpty(m_spawnedAtlasPath))
{
AssetDatabase.DeleteAsset(m_spawnedAtlasPath);
}
m_target = null;
m_spawnedAtlasPath = string.Empty;
m_spriteRects = null;
}

private Texture2D[] GatherTextures()
{
List<Texture2D> resultTexture = new List<Texture2D>();
for (int i = 0, length = _spriteToPack.Length; i < length; i++)
{
Sprite existSprite = _spriteToPack[i];
string assetPath = AssetDatabase.GetAssetPath(existSprite);
if (string.IsNullOrEmpty(assetPath))
{
continue;
}
Texture2D loadedT2D = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
resultTexture.Add(loadedT2D);
}
return resultTexture.ToArray();
}

private void FixImportSettings(Texture2D[] textures, bool readable)
{
for (int i = 0, length = textures.Length; i < length; i++)
{
Texture2D texture = textures[i];
string path = AssetDatabase.GetAssetPath(texture);
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;

if (importer == null) continue;
importer.isReadable = readable;
importer.SaveAndReimport();
}
}

private Rect ConvertToPixelRect(int width, int height, Rect uvRect)
{
float pixelLeft = uvRect.x * width;
float pixelTop = uvRect.y * height;
float pixelWidth = uvRect.width * width;
float pixelHeight = uvRect.height * height;
return new Rect(pixelLeft, pixelTop, pixelWidth, pixelHeight);
}

private Texture2D DeCompress(Texture2D source)
{
RenderTexture renderTex = RenderTexture.GetTemporary(
source.width,
source.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear);

Graphics.Blit(source, renderTex);
RenderTexture previous = RenderTexture.active;
RenderTexture.active = renderTex;
Texture2D readableText = new Texture2D(source.width, source.height);
readableText.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0);
readableText.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(renderTex);
return readableText;
}

private void AddDefaultMaterial(TMP_SpriteAsset spriteAsset)
{
Shader shader = Shader.Find("TextMeshPro/Sprite");
Material material = new Material(shader);
material.SetTexture(ShaderUtilities.ID_MainTex, spriteAsset.spriteSheet);

spriteAsset.material = material;
material.hideFlags = HideFlags.HideInHierarchy;
AssetDatabase.AddObjectToAsset(material, spriteAsset);
}

private Texture2D[] GetAdjustedT2D(Texture2D[] source)
{
Texture2D[] result = new Texture2D[source.Length];
for (int i = 0, length = source.Length; i < length; i++)
{
Texture2D after = GPUTextureScaler.GetScaledResult(source[i], _iconSize, _iconSize);
result[i] = after;
}
return result;
}

}

// from https://discussions.unity.com/t/how-to-resize-scale-down-texture-without-losing-quality/810223/14
public class GPUTextureScaler
{
/// <summary>
/// Returns a scaled copy of given texture.
/// </summary>
/// <param name="tex">Source texure to scale</param>
/// <param name="width">Destination texture width</param>
/// <param name="height">Destination texture height</param>
/// <param name="mode">Filtering mode</param>
public static Texture2D GetScaledResult(Texture2D src, int width, int height, FilterMode mode = FilterMode.Trilinear)
{
Rect texR = new(0, 0, width, height);
DoGPUScale(src, width, height, mode);

//Get rendered data back to a new texture
Texture2D result = new(width, height, TextureFormat.ARGB32, true);
result.Reinitialize(width, height);
result.ReadPixels(texR, 0, 0, true);
result.name = src.name;
return result;
}

/// <summary>
/// Scales the texture data of the given texture.
/// </summary>
/// <param name="tex">Texure to scale</param>
/// <param name="width">New width</param>
/// <param name="height">New height</param>
/// <param name="mode">Filtering mode</param>
public static void Scale(Texture2D tex, int width, int height, FilterMode mode = FilterMode.Trilinear)
{
Rect texR = new(0, 0, width, height);
DoGPUScale(tex, width, height, mode);

// Update new texture
tex.Reinitialize(width, height);
tex.ReadPixels(texR, 0, 0, true);
tex.Apply(true); //Remove this if you hate us applying textures for you :)
}

// Internal unility that renders the source texture into the RTT - the scaling method itself.
private static void DoGPUScale(Texture2D src, int width, int height, FilterMode fmode)
{
//We need the source texture in VRAM because we render with it
src.filterMode = fmode;
src.Apply(true);

//Using RTT for best quality and performance. Thanks, Unity 5
RenderTexture rtt = new(width, height, 32);

//Set the RTT in order to render to it
Graphics.SetRenderTarget(rtt);

//Setup 2D matrix in range 0..1, so nobody needs to care about sized
GL.LoadPixelMatrix(0, 1, 1, 0);

//Then clear & draw the texture to fill the entire RTT.
GL.Clear(true, true, new Color(0, 0, 0, 0));
Graphics.DrawTexture(new Rect(0, 0, 1, 1), src);
}
}
}