1. 多轴向视图支持
- Front/Back: 前/后视图(±Z轴)
- Left/Right: 左/右视图(±X轴)
- Top/Bottom: 顶/底视图(±Y轴)
- Isometric: 等轴测视图(-X, -Y, -Z方向)
- IsometricOpposite: 相反方向的等轴测视图
2. 相机控制
- Auto Center: 自动根据轴向定位相机
- Custom Rotation: 手动设置旋转角度
3. 预览功能
- 在场景中创建临时实例
- 自动定位场景相机
- 方便调整角度后导出
4. 自动灯光
- 如果没有光源,自动创建基本的三点光源
- 确保物体被正确照亮
使用说明:
- 选择轴向:从下拉菜单中选择需要的视图方向
- 调整设置:分辨率、背景色、边距等
- 预览:点击"Preview in Scene"查看效果
- 导出:选择单个或多个Prefab导出
轴向后缀:
导出的文件名会自动添加轴向信息,如:
EmperorWalkingOverThere_Front_thumbnail.pngEmperorWalkingOverThere_Top_thumbnail.pngEmperorWalkingOverThere_Isometric_thumbnail.png
脚本代码
using UnityEditor;
using UnityEngine;
using System.IO;
public class ThumbnailExportSettings : EditorWindow
{
public int width = 256;
public int height = 256;
public bool transparentBackground = true;
public Color backgroundColor = Color.clear;
public int padding = 10;
// 轴向选择
public enum ViewAxis
{
Front,
Back,
Left,
Right,
Top,
Bottom,
Isometric,
IsometricOpposite
}
public ViewAxis selectedAxis = ViewAxis.Front;
public bool autoCenter = true;
public Vector3 customRotation = Vector3.zero;
[MenuItem("Tools/Prefab Thumbnail Exporter")]
static void ShowWindow()
{
GetWindow<ThumbnailExportSettings>("Thumbnail Exporter");
}
void OnGUI()
{
GUILayout.Label("Thumbnail Settings", EditorStyles.boldLabel);
width = EditorGUILayout.IntField("Width", Mathf.Max(1, width));
height = EditorGUILayout.IntField("Height", Mathf.Max(1, height));
transparentBackground = EditorGUILayout.Toggle("Transparent Background", transparentBackground);
if (!transparentBackground)
{
backgroundColor = EditorGUILayout.ColorField("Background Color", backgroundColor);
}
padding = EditorGUILayout.IntField("Padding", Mathf.Max(0, padding));
GUILayout.Space(10);
GUILayout.Label("Camera Settings", EditorStyles.boldLabel);
selectedAxis = (ViewAxis)EditorGUILayout.EnumPopup("View Axis", selectedAxis);
if (selectedAxis == ViewAxis.Isometric || selectedAxis == ViewAxis.IsometricOpposite)
{
EditorGUILayout.HelpBox("Isometric view combines multiple axes", MessageType.Info);
}
autoCenter = EditorGUILayout.Toggle("Auto Center Camera", autoCenter);
if (!autoCenter)
{
customRotation = EditorGUILayout.Vector3Field("Custom Rotation", customRotation);
}
GUILayout.Space(10);
if (GUILayout.Button("Export Selected Prefab", GUILayout.Height(30)))
{
ExportSelectedPrefab();
}
if (GUILayout.Button("Export All Selected Prefabs", GUILayout.Height(30)))
{
ExportAllSelectedPrefabs();
}
if (GUILayout.Button("Preview in Scene", GUILayout.Height(25)))
{
PreviewSelectedPrefab();
}
GUILayout.Space(10);
GUILayout.Label("Tips:", EditorStyles.boldLabel);
GUILayout.Label("• Front: +Z direction", EditorStyles.miniLabel);
GUILayout.Label("• Back: -Z direction", EditorStyles.miniLabel);
GUILayout.Label("• Left: -X direction", EditorStyles.miniLabel);
GUILayout.Label("• Right: +X direction", EditorStyles.miniLabel);
GUILayout.Label("• Top: +Y direction", EditorStyles.miniLabel);
GUILayout.Label("• Bottom: -Y direction", EditorStyles.miniLabel);
}
void ExportSelectedPrefab()
{
GameObject selectedPrefab = Selection.activeObject as GameObject;
if (selectedPrefab == null)
{
Debug.LogError("Please select a Prefab in the Project window!");
return;
}
string savePath = EditorUtility.SaveFilePanel(
"Save Thumbnail",
"",
$"{selectedPrefab.name}_{selectedAxis}_thumbnail",
"png");
if (!string.IsNullOrEmpty(savePath))
{
Texture2D thumbnail = CapturePrefabThumbnail(selectedPrefab, width, height);
if (thumbnail != null)
{
byte[] bytes = thumbnail.EncodeToPNG();
File.WriteAllBytes(savePath, bytes);
DestroyImmediate(thumbnail);
Debug.Log($"Thumbnail saved to: {savePath}");
}
}
}
void ExportAllSelectedPrefabs()
{
string outputFolder = EditorUtility.SaveFolderPanel("Select Output Folder", "", "");
if (string.IsNullOrEmpty(outputFolder)) return;
int processed = 0;
int total = Selection.objects.Length;
for (int i = 0; i < total; i++)
{
var obj = Selection.objects[i];
if (obj is GameObject)
{
EditorUtility.DisplayProgressBar("Exporting Thumbnails",
$"Processing: {obj.name} ({i + 1}/{total})",
(float)i / total);
Texture2D thumbnail = CapturePrefabThumbnail(obj as GameObject, width, height);
if (thumbnail != null)
{
byte[] bytes = thumbnail.EncodeToPNG();
string fileName = GetValidFileName($"{obj.name}_{selectedAxis}") + ".png";
File.WriteAllBytes(Path.Combine(outputFolder, fileName), bytes);
DestroyImmediate(thumbnail);
processed++;
}
if (i % 5 == 0) System.Threading.Thread.Sleep(10);
}
}
EditorUtility.ClearProgressBar();
Debug.Log($"Finished exporting {processed} out of {total} thumbnails");
}
void PreviewSelectedPrefab()
{
GameObject selectedPrefab = Selection.activeObject as GameObject;
if (selectedPrefab == null)
{
Debug.LogError("Please select a Prefab in the Project window!");
return;
}
// 在场景中创建临时实例用于预览
GameObject previewInstance = PrefabUtility.InstantiatePrefab(selectedPrefab) as GameObject;
if (previewInstance == null)
{
previewInstance = Object.Instantiate(selectedPrefab);
}
// 定位场景相机
SceneView.lastActiveSceneView.Frame(previewInstance.GetBounds(), true);
Debug.Log($"Preview instance created for {selectedPrefab.name}. Press Play to capture or delete the instance manually.");
}
Texture2D CapturePrefabThumbnail(GameObject prefab, int width, int height)
{
if (prefab == null) return null;
GameObject tempInstance = null;
RenderTexture rt = null;
Camera camera = null;
GameObject cameraGO = null;
try
{
// 实例化Prefab
tempInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
if (tempInstance == null)
{
tempInstance = Object.Instantiate(prefab);
}
// 计算边界
Bounds bounds = CalculateBounds(tempInstance);
if (bounds.size == Vector3.zero)
{
Debug.LogWarning($"Prefab {prefab.name} has no renderable bounds");
return null;
}
// 计算相机距离和位置
float maxSize = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z);
float distance = maxSize * 0.5f + padding * 0.01f;
// 设置渲染纹理
rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
// 创建相机
cameraGO = new GameObject("ThumbnailCamera");
camera = cameraGO.AddComponent<Camera>();
camera.clearFlags = transparentBackground ?
CameraClearFlags.SolidColor : CameraClearFlags.Color;
camera.backgroundColor = transparentBackground ? Color.clear : backgroundColor;
camera.orthographic = true;
camera.orthographicSize = distance;
camera.targetTexture = rt;
camera.nearClipPlane = 0.01f;
camera.farClipPlane = 1000f;
// 设置相机位置和旋转
Vector3 cameraPosition = bounds.center;
Quaternion cameraRotation = GetCameraRotation(selectedAxis);
if (autoCenter)
{
// 根据轴向计算相机位置
Vector3 direction = cameraRotation * Vector3.back;
cameraPosition += direction * (distance * 2f);
}
else
{
cameraGO.transform.rotation = Quaternion.Euler(customRotation);
cameraPosition += cameraGO.transform.forward * -(distance * 2f);
}
cameraGO.transform.position = cameraPosition;
cameraGO.transform.LookAt(bounds.center);
// 可选:添加简单的灯光
SetupBasicLighting(tempInstance);
// 渲染
camera.Render();
// 读取像素
Texture2D result = new Texture2D(width, height, TextureFormat.RGBA32, false);
RenderTexture.active = rt;
result.ReadPixels(new Rect(0, 0, width, height), 0, 0);
result.Apply();
RenderTexture.active = null;
return result;
}
catch (System.Exception e)
{
Debug.LogError($"Error capturing thumbnail for {prefab.name}: {e.Message}");
return null;
}
finally
{
// 清理资源
if (tempInstance != null) DestroyImmediate(tempInstance);
if (cameraGO != null) DestroyImmediate(cameraGO);
if (rt != null) RenderTexture.ReleaseTemporary(rt);
}
}
Quaternion GetCameraRotation(ViewAxis axis)
{
switch (axis)
{
case ViewAxis.Front:
return Quaternion.LookRotation(Vector3.forward, Vector3.up);
case ViewAxis.Back:
return Quaternion.LookRotation(Vector3.back, Vector3.up);
case ViewAxis.Left:
return Quaternion.LookRotation(Vector3.left, Vector3.up);
case ViewAxis.Right:
return Quaternion.LookRotation(Vector3.right, Vector3.up);
case ViewAxis.Top:
return Quaternion.LookRotation(Vector3.up, Vector3.forward);
case ViewAxis.Bottom:
return Quaternion.LookRotation(Vector3.down, Vector3.back);
case ViewAxis.Isometric:
return Quaternion.LookRotation(new Vector3(-1, -0.5f, -1).normalized, Vector3.up);
case ViewAxis.IsometricOpposite:
return Quaternion.LookRotation(new Vector3(1, -0.5f, 1).normalized, Vector3.up);
default:
return Quaternion.identity;
}
}
void SetupBasicLighting(GameObject target)
{
// 检查是否已有光源
Light[] existingLights = Object.FindObjectsOfType<Light>();
if (existingLights.Length == 0)
{
// 创建基本的三点光源
CreateLight(new Vector3(-5, 5, 5), 1.0f, Color.white); // 主光
CreateLight(new Vector3(5, 3, 5), 0.5f, new Color(0.8f, 0.8f, 1.0f)); // 辅光
CreateLight(new Vector3(0, 10, 0), 0.3f, Color.white); // 顶光
}
}
void CreateLight(Vector3 position, float intensity, Color color)
{
GameObject lightGO = new GameObject("ThumbnailLight");
Light light = lightGO.AddComponent<Light>();
light.type = LightType.Directional;
lightGO.transform.position = position;
lightGO.transform.LookAt(Vector3.zero);
light.intensity = intensity;
light.color = color;
light.shadows = LightShadows.None; // 为了性能,禁用阴影
}
Bounds CalculateBounds(GameObject obj)
{
Bounds bounds = new Bounds(obj.transform.position, Vector3.zero);
bool hasBounds = false;
// 获取所有Renderer组件
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
if (renderers.Length > 0)
{
bounds = renderers[0].bounds;
hasBounds = true;
foreach (Renderer renderer in renderers)
{
bounds.Encapsulate(renderer.bounds);
}
}
// 如果没有Renderer,使用Collider
if (!hasBounds)
{
Collider[] colliders = obj.GetComponentsInChildren<Collider>();
if (colliders.Length > 0)
{
bounds = colliders[0].bounds;
hasBounds = true;
foreach (Collider collider in colliders)
{
bounds.Encapsulate(collider.bounds);
}
}
}
// 如果仍然没有边界,使用MeshFilter
if (!hasBounds)
{
MeshFilter[] meshFilters = obj.GetComponentsInChildren<MeshFilter>();
if (meshFilters.Length > 0 && meshFilters[0].sharedMesh != null)
{
bounds = meshFilters[0].sharedMesh.bounds;
bounds.center = obj.transform.position;
hasBounds = true;
foreach (MeshFilter meshFilter in meshFilters)
{
if (meshFilter.sharedMesh != null)
{
bounds.Encapsulate(meshFilter.sharedMesh.bounds);
}
}
}
}
return bounds;
}
string GetValidFileName(string fileName)
{
char[] invalidChars = Path.GetInvalidFileNameChars();
foreach (char c in invalidChars)
{
fileName = fileName.Replace(c.ToString(), "");
}
return fileName;
}
}
// 扩展方法用于计算GameObject的边界
public static class GameObjectExtensions
{
public static Bounds GetBounds(this GameObject obj)
{
Bounds bounds = new Bounds(obj.transform.position, Vector3.zero);
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
if (renderers.Length > 0)
{
bounds = renderers[0].bounds;
foreach (Renderer renderer in renderers)
{
bounds.Encapsulate(renderer.bounds);
}
}
return bounds;
}
}
