# UnityPreviewEditor UnityPreviewEditor:使用PreviewRenderUtility创建预览窗口。 Unity 版本: Unity 2020.3.18f1c1 * [UnityPreviewEditor](#unityprevieweditor) * [简介](#简介) * [代码](#代码) * [Unity 预览界面](#unity-预览界面) * [预览界面](#预览界面) * [检视器预览界面](#检视器预览界面) * [开启预览功能](#开启预览功能) * [标题栏绘制](#标题栏绘制) * [预览内容的绘制](#预览内容的绘制) * [摄像机渲染](#摄像机渲染) * [基础绘制](#基础绘制) * [拖动旋转](#拖动旋转) * [自定义视图的预览](#自定义视图的预览) * [开源代码](#开源代码) ### 简介 1. Example -> PreviewRenderWindow 打开 2. 效果图: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/11_21_27_21_20220111212720.png) ### 代码
查看代码 ```csharp // PreviewRenderWindow.cs 负责自定义管理界面 using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; public class PreviewRenderWindow : EditorWindow { [MenuItem("Example/PreviewRenderWindow")] static void ShowWindow() { GetWindow("PreviewRenderWindow").Show(); } GameObject _gameObject; GameObject _lastGameObject; PreviewRenderEditor _editor; bool _load = true; Vector2 _lightRot; Vector2 _lastLightRot; void OnGUI() { _gameObject = (GameObject) EditorGUILayout.ObjectField("预览预制体", _gameObject, typeof(GameObject), true); _lightRot = EditorGUILayout.Vector2Field("光源方向", _lightRot); if (_editor == null) { _editor = Editor.CreateEditor(this, typeof(PreviewRenderEditor)) as PreviewRenderEditor; } if(_editor) { if (_lastLightRot != _lightRot) { _lastLightRot = _lightRot; _editor.RefreshLightRot(_lightRot); } _editor.DrawPreview(GUILayoutUtility.GetRect(400, 400)); } if (_gameObject && _load) { _editor.RefreshPreviewInstance(_gameObject); _load = false; _lastGameObject = _gameObject; } if (_lastGameObject != _gameObject) { _load = true; } } } ``` ```csharp // PreviewRenderEditor.cs 负责预览界面 using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; public class PreviewRenderEditor : Editor { private PreviewRenderUtility _previewRenderUtility; private GameObject _previewInstance; private GameObject _targetObj; private static bool _loaded = true; private Vector2 _drag = new Vector2(250f, -30f); private Vector2 _lightRot = new Vector2(180f, 0); public void RefreshLightRot(Vector2 rot) { _lightRot = rot; } public void RefreshPreviewInstance(GameObject obj) { _targetObj = obj; if (_previewInstance) UnityEngine.Object.DestroyImmediate(_previewInstance); _previewInstance = null; _loaded = true; } private void OnEnable() { if (_previewRenderUtility == null) { _previewRenderUtility = new PreviewRenderUtility(); } } private void OnDisable() { if (_previewRenderUtility != null) { // 必须进行清理,否则会存在残留对象 _previewInstance = null; _previewRenderUtility.Cleanup(); _previewRenderUtility = null; } } public override void OnPreviewGUI(Rect r, GUIStyle background) { // _loaded 确保只加载一次物体 if (_loaded && _targetObj) { _previewInstance = Instantiate(_targetObj as GameObject, Vector3.zero, Quaternion.identity); // AddSingleGO 添加物体 _previewRenderUtility.AddSingleGO(_previewInstance); _loaded = false; } // 获取拖拽向量 _drag = Drag2D(_drag, r); // 事件为绘制时,才进行绘制 if (Event.current.type == EventType.Repaint) { _previewRenderUtility.BeginPreview(r, background); //调整相机位置与角度 Camera camera = _previewRenderUtility.camera; var cameraTran = camera.transform; cameraTran.position = Vector2.zero; cameraTran.rotation = Quaternion.Euler(new Vector3(-_drag.y, -_drag.x, 0)); cameraTran.position = cameraTran.forward * -6f; var pos = cameraTran.position; cameraTran.position = new Vector3(pos.x, pos.y + 0.6f, pos.z); EditorUtility.SetCameraAnimateMaterials(camera, true); camera.cameraType = CameraType.Preview; camera.enabled = false; camera.clearFlags = CameraClearFlags.Skybox; camera.fieldOfView = 30; camera.farClipPlane = 10.0f; camera.nearClipPlane = 2.0f; camera.backgroundColor = new Color(49.0f / 255.0f, 77.0f / 255.0f, 121.0f / 255.0f, 0f); // // 设置光源数据 _previewRenderUtility.lights[0].intensity = 0.7f; _previewRenderUtility.lights[0].transform.rotation = Quaternion.Euler(_lightRot.x, _lightRot.y, 0f); _previewRenderUtility.lights[1].intensity = 0.7f; _previewRenderUtility.lights[1].transform.rotation = Quaternion.Euler(_lightRot.x, _lightRot.y, 0f); _previewRenderUtility.ambientColor = new Color(0.3f, 0.3f, 0.3f, 0f); // camera.transform.LookAt(_previewInstance.transform); // 相机渲染 camera.Render(); // 结束并绘制 _previewRenderUtility.EndAndDrawPreview(r); } } // Drag2D 来自源码 private static int sliderHash = "Slider".GetHashCode(); public static Vector2 Drag2D(Vector2 scrollPosition, Rect position) { // 每次获得独一无二的 controlID int controlID = GUIUtility.GetControlID(sliderHash, FocusType.Passive); Event current = Event.current; // 获取对应 controlID 的事件 switch (current.GetTypeForControl(controlID)) { case EventType.MouseDown: { bool flag = position.Contains(current.mousePosition) && position.width > 50f; if (flag) { // 鼠标摁住拖出预览窗口外,预览物体任然能够旋转 GUIUtility.hotControl = controlID; // 采用事件 current.Use(); // 让鼠标可以拖动到屏幕外后,从另一边出来 EditorGUIUtility.SetWantsMouseJumping(1); } break; } case EventType.MouseUp: { bool flag2 = GUIUtility.hotControl == controlID; if (flag2) { GUIUtility.hotControl = 0; } EditorGUIUtility.SetWantsMouseJumping(0); break; } case EventType.MouseDrag: { bool flag3 = GUIUtility.hotControl == controlID; if (flag3) { // shift 加速 scrollPosition -= current.delta * (float) (current.shift ? 3 : 1) / Mathf.Min(position.width, position.height) * 140f; // 以下两条缺少任意一个,会导致延迟更新,拖动过程中无法实时更新 // 直到 repaint事件触发才重新绘制 current.Use(); GUI.changed = true; } break; } } return scrollPosition; } } ```
### Unity 预览界面 #### 预览界面 在Unity编辑器界面上可以看到除了Game视图、Scene视图,其他的视图也会出现绘制三维物体的地方,比如检视器的预览窗口,当选中网格时,会对网格进行预览,如下所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/12_20_46_52_20220112204650.png) 绘制的方法都是使用 UnityEditor 未公开文档的PreviewRenderUtility类进行的。 #### 检视器预览界面 资产或脚本实现预览窗口可参考Editor类的文档说明,重载带有Preview关键字的接口。 #### 开启预览功能 默认脚本对象的检视器窗口是没有预览窗口的,如下所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/12_20_51_42_20220112205141.png) 想要开启预览窗口,那么得创建自己的检视器窗口类,然后重载HasPreviewGUI接口,完整代码如下: ```csharp using UnityEngine; public class PreviewExample : MonoBehaviour { } using UnityEditor; using UnityEngine; [CustomEditor(typeof(PreviewExample))] public class PreviewExampleInspector : Editor { public override bool HasPreviewGUI() { return true; } } ``` 可以看到有黑色的预览窗口了,如下所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/12_21_4_19_20220112210419.png) #### 标题栏绘制 默认显示的是物体的名称,重载 GetPreviewTitle 接口可以更改标题名称: ```csharp public override GUIContent GetPreviewTitle() { return new GUIContent("预览"); } ``` 标题栏右边可以绘制其他的信息或者按钮等,重载 OnPreviewSettings 接口方便对预览窗口进行控制: ```csharp public override void OnPreviewSettings() { GUILayout.Label("文本", "preLabel"); GUILayout.Button("按钮", "preButton"); } ``` #### 预览内容的绘制 最后预览内容的绘制,只需要重载 OnPreviewGUI 接口即可: ```csharp public override void OnPreviewGUI(Rect r, GUIStyle background) { GUI.Box(r, "Preview"); } ``` 最后显示如下所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/12_21_15_5_20220112211505.png) #### 摄像机渲染 不仅仅在预览窗口进行绘制控件,还可以绘制三维物体,实质是绘制独立的摄像机所照射的信息,例如动画片段预览窗口: 鼠标可以拖动旋转等,还可以看其他方向,就像操作摄像机一样。 这都是通过 PreviewRenderUtility 来实现的,对于这个类没有官方文档,可以通过网上其他人的分享,还有 UnityEditor 内部的使用来学习。 #### 基础绘制 PreviewRenderUtility 的构造和销毁,还有要预览物体的构造和销毁,以及调用绘制,以 BeginPreview 和 EndAndDrawPreview 包围,在其中进行摄像机的渲染 Camera.Render 调用,代码如下:
查看代码 ```csharp using UnityEditor; using UnityEngine; [CustomEditor(typeof(PreviewExample))] public class PreviewExampleInspector : Editor { private GameObject _lastGameObj; private bool _canRefreshPreviewGo = false; public override void OnInspectorGUI() { // target 当前操作的对象 PreviewExample pe = (PreviewExample) target; pe.previewGo = EditorGUILayout.ObjectField("预览目标", pe.previewGo, typeof(GameObject)) as GameObject; if (pe.previewGo != _lastGameObj) { _lastGameObj = pe.previewGo; _canRefreshPreviewGo = true; } serializedObject.ApplyModifiedProperties(); } public override bool HasPreviewGUI() { return true; } public override GUIContent GetPreviewTitle() { return new GUIContent("预览"); } public override void OnPreviewSettings() { GUILayout.Label("文本", "preLabel"); GUILayout.Button("按钮", "preButton"); } public override void OnPreviewGUI(Rect r, GUIStyle background) { InitPreview(); if (Event.current.type != EventType.Repaint) { return; } _previewRenderUtility.BeginPreview(r, background); Camera camera = _previewRenderUtility.camera; if (_previewInstance) { camera.transform.position = _previewInstance.transform.position + new Vector3(0, 5f, 3f); camera.transform.LookAt(_previewInstance.transform); } camera.Render(); _previewRenderUtility.EndAndDrawPreview(r); } private PreviewRenderUtility _previewRenderUtility; private GameObject _previewInstance; private void InitPreview() { if (_previewRenderUtility == null) { // 参数true代表绘制场景内的游戏对象 _previewRenderUtility = new PreviewRenderUtility(true); // 设置摄像机的一些参数 _previewRenderUtility.cameraFieldOfView = 30f; } if (_canRefreshPreviewGo) { _canRefreshPreviewGo = false; // 创建预览的游戏对象 CreatePreviewInstances(); } } private void DestroyPreview() { if (_previewRenderUtility != null) { // 务必要进行清理,才不会残留生成的摄像机对象等 _previewRenderUtility.Cleanup(); _previewRenderUtility = null; } } private void CreatePreviewInstances() { DestroyPreviewInstances(); // 绘制预览的游戏对象 if (_lastGameObj) { _previewInstance = Instantiate(_lastGameObj); _previewRenderUtility.AddSingleGO(_previewInstance); } } private void DestroyPreviewInstances() { if (_previewInstance) { DestroyImmediate(_previewInstance); } _previewInstance = null; } void OnDestroy() { DestroyPreviewInstances(); DestroyPreview(); } } ```
最后效果如下所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/13_10_13_40_20220113101338.png) #### 拖动旋转 在预览窗口鼠标拖动可以旋转进行预览,就像Cube物体预览一样。要想让摄像机旋转,得知道游戏对象的中心,才能绕着它进行旋转。
查看代码 ```csharp private static int sliderHash = "Slider".GetHashCode(); private static Vector2 Drag2D(Vector2 scrollPosition, Rect position) { // 每次获得独一无二的 controlID int controlID = GUIUtility.GetControlID(sliderHash, FocusType.Passive); Event current = Event.current; // 获取对应 controlID 的事件 switch (current.GetTypeForControl(controlID)) { case EventType.MouseDown: { bool flag = position.Contains(current.mousePosition) && position.width > 50f; if (flag) { // 鼠标摁住拖出预览窗口外,预览物体任然能够旋转 GUIUtility.hotControl = controlID; // 采用事件 current.Use(); // 让鼠标可以拖动到屏幕外后,从另一边出来 EditorGUIUtility.SetWantsMouseJumping(1); } break; } case EventType.MouseUp: { bool flag2 = GUIUtility.hotControl == controlID; if (flag2) { GUIUtility.hotControl = 0; } EditorGUIUtility.SetWantsMouseJumping(0); break; } case EventType.MouseDrag: { bool flag3 = GUIUtility.hotControl == controlID; if (flag3) { // shift 加速 scrollPosition -= current.delta * (float) (current.shift ? 3 : 1) / Mathf.Min(position.width, position.height) * 140f; // 以下两条缺少任意一个,会导致延迟更新,拖动过程中无法实时更新 // 直到 repaint事件触发才重新绘制 current.Use(); GUI.changed = true; } break; } } return scrollPosition; } ```
最后效果,如下图所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/13_10_48_42_20220113104841.png) #### 自定义视图的预览 在自定义视图上的预览,可以采用类似以上的方式进行绘制,也可以创建相应的检视器类,直接调用绘制预览接口。代码如下:
查看代码 ```csharp using UnityEngine; using UnityEditor; public class PreviewExampleWindow : EditorWindow { private Editor m_Editor; [MenuItem("Example/PreviewExample")] static void ShowWindow() { GetWindow("PreviewExample"); } private void OnDestroy() { if (m_Editor != null) { DestroyImmediate(m_Editor); } m_Editor = null; } void OnGUI() { if (m_Editor == null) { // 第一个参数这里暂时没关系,因为编辑器没有取目标对象 m_Editor = Editor.CreateEditor(this, typeof(PreviewExampleInspector)); } m_Editor.DrawPreview(GUILayoutUtility.GetRect(300, 200)); } } ```
打开测试窗口,如下图所示: ![](https://gitcode.net/hankangwen/blog-image/-/raw/master/pictures/2022/01/13_11_3_33_20220113110332.png) ### 开源代码 [https://gitcode.net/hankangwen/unityprevieweditor](https://gitcode.net/hankangwen/unityprevieweditor)