UnityPreviewEditor:使用PreviewRenderUtility创建预览窗口

UnityPreviewEditor

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

简介

  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
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kerven_HKW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值