在Unity项目开发中,我们经常需要处理大量3D模型、材质、特效预制体等资产。默认的Inspector预览窗口功能有限,特别是当我们需要同时对比多个资产,或者需要调整光照参数时,就显得捉襟见肘了。我遇到过不少这样的情况:美术同学调整了一个材质球,但要在场景中反复切换才能看到最终效果,效率非常低。
PreviewRenderUtility是Unity提供的一个强大但鲜为人知的工具类,它允许我们在编辑器窗口中创建完全自定义的3D预览。不同于Scene视图,它更轻量级,可以同时展示多个预览,而且不会影响实际场景。我在一个角色换装系统中就用到了这个技术,让策划能够直观地看到不同装备组合的效果,大大提升了工作效率。
我们先从最基本的窗口结构开始。创建一个继承自EditorWindow的类,这是所有自定义编辑器窗口的基础:
csharp复制using UnityEditor;
using UnityEngine;
public class AssetPreviewWindow : EditorWindow
{
[MenuItem("Tools/Asset Preview")]
static void Init()
{
var window = GetWindow<AssetPreviewWindow>();
window.titleContent = new GUIContent("Asset Preview");
window.Show();
}
void OnGUI()
{
// 预览内容将在这里绘制
}
}
这个基础框架已经可以显示一个空窗口了。接下来我们需要引入PreviewRenderUtility,它是实现3D预览的核心。这里有个坑要注意:PreviewRenderUtility实例必须手动清理,否则会导致内存泄漏。我吃过这个亏,编辑器运行时间长了就会变得特别卡。
在窗口类中添加以下字段和方法:
csharp复制private PreviewRenderUtility previewUtility;
private GameObject previewObject;
void InitializePreview()
{
if (previewUtility == null)
{
previewUtility = new PreviewRenderUtility();
previewUtility.camera.fieldOfView = 30f;
previewUtility.camera.nearClipPlane = 0.1f;
previewUtility.camera.farClipPlane = 10f;
}
}
void CleanupPreview()
{
if (previewUtility != null)
{
previewUtility.Cleanup();
previewUtility = null;
}
}
记得在窗口关闭时清理资源:
csharp复制void OnDestroy()
{
CleanupPreview();
if (previewObject != null)
{
DestroyImmediate(previewObject);
}
}
现在我们来添加选择模型的功能。在OnGUI方法中添加:
csharp复制void OnGUI()
{
// 选择要预览的模型
var newObj = (GameObject)EditorGUILayout.ObjectField("Preview Model", previewObject, typeof(GameObject), false);
if (newObj != previewObject)
{
if (previewObject != null)
DestroyImmediate(previewObject);
previewObject = Instantiate(newObj);
previewUtility.AddSingleGO(previewObject);
}
// 绘制预览区域
Rect previewRect = GUILayoutUtility.GetRect(400, 400, GUILayout.ExpandWidth(true));
DrawPreview(previewRect);
}
DrawPreview方法的实现是关键:
csharp复制void DrawPreview(Rect rect)
{
InitializePreview();
if (previewObject == null)
{
EditorGUI.DrawRect(rect, new Color(0.1f, 0.1f, 0.1f));
return;
}
if (Event.current.type == EventType.Repaint)
{
previewUtility.BeginPreview(rect, GUIStyle.none);
// 设置相机位置
previewUtility.camera.transform.position = -Vector3.forward * 5f;
previewUtility.camera.transform.LookAt(Vector3.zero);
// 设置光源
previewUtility.lights[0].intensity = 0.8f;
previewUtility.lights[0].transform.rotation = Quaternion.Euler(30f, 30f, 0f);
previewUtility.camera.Render();
previewUtility.EndAndDrawPreview(rect);
}
}
没有交互的预览窗口就像没有方向盘的汽车。我们来添加鼠标拖动旋转和滚轮缩放功能:
csharp复制private Vector2 dragRotation;
private float zoomDistance = 5f;
void DrawPreview(Rect rect)
{
// ...之前的代码...
// 处理鼠标交互
HandleMouseInteraction(rect);
if (Event.current.type == EventType.Repaint)
{
previewUtility.BeginPreview(rect, GUIStyle.none);
// 更新相机位置,考虑缩放
previewUtility.camera.transform.position = -Vector3.forward * zoomDistance;
previewUtility.camera.transform.rotation = Quaternion.Euler(new Vector3(-dragRotation.y, -dragRotation.x, 0));
// ...其他设置...
previewUtility.camera.Render();
previewUtility.EndAndDrawPreview(rect);
}
}
void HandleMouseInteraction(Rect rect)
{
int controlID = GUIUtility.GetControlID(FocusType.Passive);
Event evt = Event.current;
switch (evt.type)
{
case EventType.MouseDown:
if (rect.Contains(evt.mousePosition))
{
GUIUtility.hotControl = controlID;
evt.Use();
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl == controlID)
{
dragRotation -= evt.delta * 0.5f;
evt.Use();
}
break;
case EventType.ScrollWheel:
if (rect.Contains(evt.mousePosition))
{
zoomDistance = Mathf.Clamp(zoomDistance + evt.delta.y * 0.1f, 1f, 10f);
evt.Use();
}
break;
}
}
为了让预览更加专业,我们可以添加对光源的控制。在窗口类中添加:
csharp复制private Vector2 light1Rotation = new Vector2(30f, 30f);
private float light1Intensity = 0.8f;
private Color light1Color = Color.white;
void OnGUI()
{
// ...之前的UI代码...
EditorGUILayout.Space();
EditorGUILayout.LabelField("Light Settings", EditorStyles.boldLabel);
light1Rotation = EditorGUILayout.Vector2Field("Light Rotation", light1Rotation);
light1Intensity = EditorGUILayout.Slider("Intensity", light1Intensity, 0f, 2f);
light1Color = EditorGUILayout.ColorField("Color", light1Color);
// ...预览绘制代码...
}
然后在DrawPreview中更新光源:
csharp复制previewUtility.lights[0].transform.rotation = Quaternion.Euler(light1Rotation.x, light1Rotation.y, 0f);
previewUtility.lights[0].intensity = light1Intensity;
previewUtility.lights[0].color = light1Color;
不同的背景色可以更好地展示模型细节。添加这些控制选项:
csharp复制private Color backgroundColor = new Color(0.1f, 0.1f, 0.1f);
private Color ambientColor = new Color(0.2f, 0.2f, 0.2f);
void OnGUI()
{
// ...其他UI...
backgroundColor = EditorGUILayout.ColorField("Background", backgroundColor);
ambientColor = EditorGUILayout.ColorField("Ambient", ambientColor);
}
void DrawPreview(Rect rect)
{
// ...其他代码...
previewUtility.camera.backgroundColor = backgroundColor;
previewUtility.ambientColor = ambientColor;
// ...渲染代码...
}
为了让团队成员可以共享相同的预览设置,我们可以添加预设功能:
csharp复制[System.Serializable]
public class PreviewSettings
{
public Vector2 lightRotation;
public float lightIntensity;
public Color lightColor;
public Color backgroundColor;
public Color ambientColor;
}
private PreviewSettings currentSettings = new PreviewSettings();
void SaveSettings()
{
string json = JsonUtility.ToJson(currentSettings);
EditorPrefs.SetString("AssetPreview_Settings", json);
}
void LoadSettings()
{
if (EditorPrefs.HasKey("AssetPreview_Settings"))
{
string json = EditorPrefs.GetString("AssetPreview_Settings");
currentSettings = JsonUtility.FromJson<PreviewSettings>(json);
}
}
然后在适当的时机调用这些方法,比如在窗口打开时加载,在设置变化时保存。
PreviewRenderUtility如果不正确清理会导致严重的内存泄漏。我建议采用这样的模式:
csharp复制void OnEnable()
{
LoadSettings();
InitializePreview();
}
void OnDisable()
{
SaveSettings();
CleanupPreview();
}
另外,实例化的预览对象也要记得销毁:
csharp复制void CleanupPreviewObjects()
{
if (previewObject != null)
{
DestroyImmediate(previewObject);
previewObject = null;
}
}
当预览复杂模型时,可以适当降低渲染质量:
csharp复制previewUtility.camera.allowMSAA = false;
previewUtility.camera.allowHDR = false;
如果预览多个对象,考虑使用LOD系统:
csharp复制var lodGroup = previewObject.GetComponent<LODGroup>();
if (lodGroup != null)
{
lodGroup.ForceLOD(0); // 强制使用最高细节级别
}
如果预览显示不正常,检查以下几点:
我在项目中遇到过材质显示异常的问题,后来发现是因为某些shader不支持编辑器预览模式。解决方法是为这些材质创建专门的预览shader变体。