Skip to content

Instantly share code, notes, and snippets.

@adrenak
Created May 31, 2026 20:36
Show Gist options
  • Select an option

  • Save adrenak/ac84b316d3168d1f669ba8ba555cea70 to your computer and use it in GitHub Desktop.

Select an option

Save adrenak/ac84b316d3168d1f669ba8ba555cea70 to your computer and use it in GitHub Desktop.
UVIslandExporterWindow (tags:unity,util) (about:easily export UV maps from SkinnedMeshRenderer and MeshFilter in Unity)
using UnityEditor;
using UnityEngine;
using System.IO;
public class UVIslandExporterWindow : EditorWindow {
[SerializeField]
private Object[] sourceObjects = new Object[0];
private int textureSize = 2048;
private int lineThickness = 2;
private Color lineColor = Color.black;
private Color backgroundColor = Color.white;
private Texture2D previewTexture;
private const double PreviewRefreshDelay = 1.0;
private double scheduledPreviewRefreshTime;
private bool isPreviewRefreshScheduled;
private readonly int[] sizeOptions = { 1024, 2048, 4096 };
private readonly string[] sizeLabels = { "1024", "2048", "4096" };
[MenuItem("Tools/UV Island Exporter")]
public static void Open() {
UVIslandExporterWindow window =
GetWindow<UVIslandExporterWindow>("UV Island Exporter");
window.minSize = new Vector2(700, 850);
window.Show();
}
[MenuItem("CONTEXT/MeshFilter/Export UV Islands")]
private static void ExportFromMeshFilter(MenuCommand command) {
UVIslandExporterWindow window =
GetWindow<UVIslandExporterWindow>("UV Island Exporter");
window.minSize = new Vector2(700, 850);
window.AddSourceObject(command.context as Object);
window.Show();
}
[MenuItem("CONTEXT/SkinnedMeshRenderer/Export UV Islands")]
private static void ExportFromSkinnedMeshRenderer(MenuCommand command) {
UVIslandExporterWindow window =
GetWindow<UVIslandExporterWindow>("UV Island Exporter");
window.minSize = new Vector2(700, 850);
window.AddSourceObject(command.context as Object);
window.Show();
}
private void OnEnable() {
EditorApplication.update -= EditorUpdate;
EditorApplication.update += EditorUpdate;
}
private void OnDisable() {
EditorApplication.update -= EditorUpdate;
ClearPreviewTexture();
}
private void OnDestroy() {
EditorApplication.update -= EditorUpdate;
ClearPreviewTexture();
}
private void AddSourceObject(Object obj) {
if (obj == null)
return;
if (sourceObjects == null)
sourceObjects = new Object[0];
foreach (Object existingObject in sourceObjects) {
if (existingObject == obj)
return;
}
Object[] newArray = new Object[sourceObjects.Length + 1];
for (int i = 0; i < sourceObjects.Length; i++)
newArray[i] = sourceObjects[i];
newArray[sourceObjects.Length] = obj;
sourceObjects = newArray;
SchedulePreviewRefresh();
Repaint();
}
private void EditorUpdate() {
if (!isPreviewRefreshScheduled)
return;
if (EditorApplication.timeSinceStartup < scheduledPreviewRefreshTime)
return;
isPreviewRefreshScheduled = false;
GeneratePreviewTexture();
}
private void SchedulePreviewRefresh() {
scheduledPreviewRefreshTime =
EditorApplication.timeSinceStartup + PreviewRefreshDelay;
isPreviewRefreshScheduled = true;
Repaint();
}
private void CancelScheduledPreviewRefresh() {
isPreviewRefreshScheduled = false;
}
private void ClearPreviewTexture() {
if (previewTexture == null)
return;
DestroyImmediate(previewTexture);
previewTexture = null;
}
private void OnGUI() {
EditorGUILayout.LabelField("Export UV Lines", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
SerializedObject serializedObject = new SerializedObject(this);
SerializedProperty sourceObjectsProperty =
serializedObject.FindProperty("sourceObjects");
EditorGUILayout.PropertyField(
sourceObjectsProperty,
new GUIContent("Mesh Sources"),
true
);
serializedObject.ApplyModifiedProperties();
int selectedIndex = System.Array.IndexOf(sizeOptions, textureSize);
if (selectedIndex < 0)
selectedIndex = 1;
selectedIndex = EditorGUILayout.Popup(
"Texture Size",
selectedIndex,
sizeLabels
);
textureSize = sizeOptions[selectedIndex];
lineThickness = EditorGUILayout.IntSlider(
"Line Thickness",
lineThickness,
1,
5
);
lineColor = EditorGUILayout.ColorField(
"Line Color",
lineColor
);
backgroundColor = EditorGUILayout.ColorField(
"Background Color",
backgroundColor
);
if (EditorGUI.EndChangeCheck()) {
SchedulePreviewRefresh();
}
EditorGUILayout.Space(10);
GUI.enabled = sourceObjects != null && sourceObjects.Length > 0;
if (GUILayout.Button("Refresh Preview", GUILayout.Height(28))) {
CancelScheduledPreviewRefresh();
GeneratePreviewTexture();
}
if (GUILayout.Button("Export UV Islands", GUILayout.Height(35))) {
ExportUVIslands();
}
GUI.enabled = true;
EditorGUILayout.Space(10);
DrawPreview();
}
private void GeneratePreviewTexture() {
ClearPreviewTexture();
previewTexture = CreateUVTexture(false);
Repaint();
}
private Texture2D CreateUVTexture(bool showDialogs) {
if (sourceObjects == null || sourceObjects.Length == 0) {
if (showDialogs) {
EditorUtility.DisplayDialog(
"No Mesh Sources",
"Please assign at least one MeshFilter or SkinnedMeshRenderer.",
"OK"
);
}
return null;
}
Texture2D texture = new Texture2D(
textureSize,
textureSize,
TextureFormat.RGBA32,
false
);
Color32 background = backgroundColor;
Color32 exportColor = lineColor;
Color32[] pixels = new Color32[textureSize * textureSize];
for (int i = 0; i < pixels.Length; i++)
pixels[i] = background;
texture.SetPixels32(pixels);
int exportedMeshCount = 0;
foreach (Object sourceObject in sourceObjects) {
Mesh mesh = GetMeshFromObject(sourceObject);
if (mesh == null)
continue;
if (mesh.uv == null || mesh.uv.Length == 0)
continue;
DrawMeshUVs(texture, mesh, exportColor);
exportedMeshCount++;
}
if (exportedMeshCount == 0) {
if (showDialogs) {
EditorUtility.DisplayDialog(
"No Valid UVs Found",
"None of the assigned objects had valid mesh UVs.",
"OK"
);
}
DestroyImmediate(texture);
return null;
}
texture.Apply();
return texture;
}
private void DrawPreview() {
EditorGUILayout.LabelField(
isPreviewRefreshScheduled
? "UV Preview (Refreshing)"
: "UV Preview",
EditorStyles.boldLabel
);
Rect previewRect = GUILayoutUtility.GetRect(
100,
100,
GUILayout.ExpandWidth(true),
GUILayout.ExpandHeight(true)
);
EditorGUI.DrawRect(previewRect, new Color(0.35f, 0.35f, 0.35f));
if (previewTexture == null) {
GUI.Label(
previewRect,
"Add mesh sources or click Refresh Preview.",
EditorStyles.centeredGreyMiniLabel
);
return;
}
float size = Mathf.Min(previewRect.width, previewRect.height);
Rect imageRect = new Rect(
previewRect.x + (previewRect.width - size) * 0.5f,
previewRect.y + (previewRect.height - size) * 0.5f,
size,
size
);
GUI.DrawTexture(
imageRect,
previewTexture,
ScaleMode.ScaleToFit,
false
);
}
private void ExportUVIslands() {
string path = EditorUtility.SaveFilePanel(
"Export UV Islands",
Application.dataPath,
"Combined_UV_Islands.png",
"png"
);
if (string.IsNullOrEmpty(path))
return;
Texture2D texture = CreateUVTexture(true);
if (texture == null)
return;
byte[] pngBytes = texture.EncodeToPNG();
File.WriteAllBytes(path, pngBytes);
EditorUtility.RevealInFinder(path);
AssetDatabase.Refresh();
DestroyImmediate(texture);
EditorUtility.DisplayDialog(
"Export Complete",
"UV island texture exported successfully.",
"OK"
);
}
private void DrawMeshUVs(Texture2D texture, Mesh mesh, Color32 color) {
Vector2[] uvs = mesh.uv;
int[] triangles = mesh.triangles;
for (int i = 0; i < triangles.Length; i += 3) {
Vector2 uv0 = uvs[triangles[i]];
Vector2 uv1 = uvs[triangles[i + 1]];
Vector2 uv2 = uvs[triangles[i + 2]];
DrawUVLine(texture, uv0, uv1, color);
DrawUVLine(texture, uv1, uv2, color);
DrawUVLine(texture, uv2, uv0, color);
}
}
private Mesh GetMeshFromObject(Object obj) {
if (obj == null)
return null;
if (obj is MeshFilter meshFilter)
return meshFilter.sharedMesh;
if (obj is SkinnedMeshRenderer skinnedMeshRenderer)
return skinnedMeshRenderer.sharedMesh;
if (obj is GameObject gameObject) {
MeshFilter meshFilterFromObject =
gameObject.GetComponent<MeshFilter>();
if (meshFilterFromObject != null)
return meshFilterFromObject.sharedMesh;
SkinnedMeshRenderer skinnedMeshRendererFromObject =
gameObject.GetComponent<SkinnedMeshRenderer>();
if (skinnedMeshRendererFromObject != null)
return skinnedMeshRendererFromObject.sharedMesh;
}
return null;
}
private void DrawUVLine(
Texture2D texture,
Vector2 uvA,
Vector2 uvB,
Color32 color
) {
int x0 = UVToPixelX(uvA.x);
int y0 = UVToPixelY(uvA.y);
int x1 = UVToPixelX(uvB.x);
int y1 = UVToPixelY(uvB.y);
DrawLine(texture, x0, y0, x1, y1, color);
}
private int UVToPixelX(float u) {
u = Mathf.Repeat(u, 1f);
return Mathf.Clamp(
Mathf.RoundToInt(u * (textureSize - 1)),
0,
textureSize - 1
);
}
private int UVToPixelY(float v) {
v = Mathf.Repeat(v, 1f);
return Mathf.Clamp(
Mathf.RoundToInt(v * (textureSize - 1)),
0,
textureSize - 1
);
}
private void DrawLine(
Texture2D texture,
int x0,
int y0,
int x1,
int y1,
Color32 color
) {
int dx = Mathf.Abs(x1 - x0);
int dy = Mathf.Abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
while (true) {
DrawThickPixel(texture, x0, y0, color);
if (x0 == x1 && y0 == y1)
break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
private void DrawThickPixel(
Texture2D texture,
int x,
int y,
Color32 color
) {
int radius = Mathf.Max(0, lineThickness - 1);
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetY = -radius; offsetY <= radius; offsetY++) {
int px = x + offsetX;
int py = y + offsetY;
if (px < 0 || px >= textureSize || py < 0 || py >= textureSize)
continue;
texture.SetPixel(px, py, color);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment