README.md

    UnityPreviewEditor

    UnityPreviewEditor:使用PreviewRenderUtility创建预览窗口。

    Unity 版本: Unity 2020.3.18f1c1

    简介

    1. Example -> PreviewRenderWindow 打开
    2. 效果图:

    代码

    查看代码
    // 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>("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;
            }
        }
    }
    // 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视图,其他的视图也会出现绘制三维物体的地方,比如检视器的预览窗口,当选中网格时,会对网格进行预览,如下所示:

    绘制的方法都是使用 UnityEditor 未公开文档的PreviewRenderUtility类进行的。

    检视器预览界面

    资产或脚本实现预览窗口可参考Editor类的文档说明,重载带有Preview关键字的接口。

    开启预览功能

    默认脚本对象的检视器窗口是没有预览窗口的,如下所示:

    想要开启预览窗口,那么得创建自己的检视器窗口类,然后重载HasPreviewGUI接口,完整代码如下:

    using UnityEngine;
    public class PreviewExample : MonoBehaviour
    {
    }
    using UnityEditor;
    using UnityEngine;
    [CustomEditor(typeof(PreviewExample))]
    public class PreviewExampleInspector : Editor
    {
        public override bool HasPreviewGUI()
        {
            return true;
        }
    }

    可以看到有黑色的预览窗口了,如下所示:

    标题栏绘制

    默认显示的是物体的名称,重载 GetPreviewTitle 接口可以更改标题名称:

    public override GUIContent GetPreviewTitle()
    {
        return new GUIContent("预览");
    }

    标题栏右边可以绘制其他的信息或者按钮等,重载 OnPreviewSettings 接口方便对预览窗口进行控制:

    public override void OnPreviewSettings()
    {
        GUILayout.Label("文本", "preLabel");
        GUILayout.Button("按钮", "preButton");
    }

    预览内容的绘制

    最后预览内容的绘制,只需要重载 OnPreviewGUI 接口即可:

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        GUI.Box(r, "Preview");
    }

    最后显示如下所示:

    摄像机渲染

    不仅仅在预览窗口进行绘制控件,还可以绘制三维物体,实质是绘制独立的摄像机所照射的信息,例如动画片段预览窗口:

    鼠标可以拖动旋转等,还可以看其他方向,就像操作摄像机一样。

    这都是通过 PreviewRenderUtility 来实现的,对于这个类没有官方文档,可以通过网上其他人的分享,还有 UnityEditor 内部的使用来学习。

    基础绘制

    PreviewRenderUtility 的构造和销毁,还有要预览物体的构造和销毁,以及调用绘制,以 BeginPreview 和 EndAndDrawPreview 包围,在其中进行摄像机的渲染 Camera.Render 调用,代码如下:

    查看代码
    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();
        }
    }

    最后效果如下所示:

    拖动旋转

    在预览窗口鼠标拖动可以旋转进行预览,就像Cube物体预览一样。要想让摄像机旋转,得知道游戏对象的中心,才能绕着它进行旋转。

    查看代码
    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;
    }
    最后效果,如下图所示:

    自定义视图的预览

    在自定义视图上的预览,可以采用类似以上的方式进行绘制,也可以创建相应的检视器类,直接调用绘制预览接口。代码如下:

    查看代码
    using UnityEngine;
    using UnityEditor;
    
    public class PreviewExampleWindow : EditorWindow
    {
        private Editor m_Editor;
    
        [MenuItem("Example/PreviewExample")]
        static void ShowWindow()
        {
            GetWindow<PreviewExampleWindow>("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/unityprevieweditor

    项目简介

    UnityPreviewEditor:使用PreviewRenderUtility创建预览窗口。

    发行版本

    当前项目没有发行版本

    贡献者 2

    Kerven_HKW @qq_34035956

    开发语言

    • C# 100.0 %