Unity下實現自定義模型預覽窗口,支持鏡頭旋轉

轉載自https://timaksu.com/post/126337219047/au.linkedin.com/in/timaksu

非常好的一篇文章,介紹了Unity下擴展預覽窗口(Preview Window)的基本流程。不過原文很多外鏈都失效了,而且用的Unity接口有一部分也已經更新了,這篇文章基本是在原文的基礎上補上失效的圖片(自己截的),同時把一些棄用的接口改成新的,在2019.1.0f2下測試通過。

代碼修改如下:

  1. PreviewRenderUtility的m_Camera成員改爲camera
  2. 原文是在OnDestroy裏做清理的,但其實應該在OnDisable
  3. 成員變量改爲m_前綴(個人習慣)

Spruce up your custom Unity inspectors with a preview area!

This is a tutorial that’ll show you how to make one of these preview areas for your own script! We’ll be creating a custom editor script that gives the MeshFilter component a preview. It’s fairly simple but it’ll arm you with everything you need to know to build your own previews like above!

To start with, lets just get that preview area to show up! It is fairly straight forward to achieve this. You’ll need to write your own custom inspector for your behaviour (This tutorial assumes you know how to do that already! Check here for a how-to). Normally you’d override the OnInspectorGUI method and define your own inspector GUI code. But there are some other methods there that we can override to define our Preview GUI!

First, we need to tell Unity that the inspector for our component does in fact have a Preview GUI. To do this, just override the HasPreviewGUI method return true! You may choose to conditionally show a preview GUI (Based on some settings parameter or variable value on the target object), and this is where you do all your checks to decide if you want to show the GUI or not. We’ll just return true and signal to Unity to always draw the preview area.

Add this script to an Editor folder in your project, create a Cube in your scene, select it and check out the inspector!

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MeshFilter))]
[CanEditMultipleObjects]
public class MeshFilterPreview : Editor
{
	public override bool HasPreviewGUI()
	{
        	return true;
	}
}

empy preview
Now, with the preview area visible, we can see exactly what part Unity will play in rendering your preview. You don’t have to worry about writing the code for a resizable preview area. Unity does all that for you and places the objects title in the bar too. If its up to you to do the rest. The next step is to override OnPreviewGUI and render our preview inside the box!

Unity will pass a Rect to your OnPreviewGUI method. This Rect represents the size and location of the dark gray box in the image above. it is up to you to decide what you want to draw in this area! For example, the UnityUI framework uses this preview area to display information about a UI elements layout properties. This is achieved just by using a few GUI.Labels and drawing those labels within that Rect. Basically, you can do all the stuff you do in OnInspectorGUI in OnPreviewGUI too.
preview of layout element
This is all well and good, but it is common to want to render some 2D or 3D graphics in a preview. How do we achieve that? Introducing the PreviewRenderUtility class. A PreviewRenderUtility is like a self contained little Unity scene with a camera and light! You can position the camera and light however you like by accessing the camera or lights transform.

You’ll need to create a new instance of a PreviewRenderUtility object and use it to render your 3D preview.

You have to use your imagination here as there is no scene view to help you out so you need to position everything by code.

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MeshFilter))]
[CanEditMultipleObjects]
public class MeshFilterPreview : Editor
{
    //We dont create a PRU every frame, keep a reference to your one!
    private PreviewRenderUtility m_PreviewRenderUtility;
    private MeshFilter m_TargetMeshFilter;
    private MeshRenderer m_TargetMeshRenderer;

    //Fetch all the relevent information
    private void ValidateData()
    {
        _targetMeshFilter = target as MeshFilter;
        _targetMeshRenderer = _targetMeshFilter.GetComponent<MeshRenderer>();
        
        if (m_PreviewRenderUtility == null) {
            m_PreviewRenderUtility = new PreviewRenderUtility();

            //We set the previews camera to 6 units back, look towards the middle of the 'scene'
            m_PreviewRenderUtility.camera.transform.position = new Vector3(0, 0, -6);
            m_PreviewRenderUtility.camera.transform.rotation = Quaternion.identity;
        }

        //We'll need the GO's mesh filter and renderer
        //to be able to render a preview of the mesh!
        m_TargetMeshFilter = target as MeshFilter;
        m_TargetMeshRenderer = m_TargetMeshFilter.GetComponent<MeshRenderer>();
    }

    public override bool HasPreviewGUI()
    {
        //Validate data - this is always called before OnPreviewGUI
        ValidateData();

        return true;
    }

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        //Only render our 3D 'preview' when the UI is 'repainting'.
        //The OnPreviewGUI, like other GUI methods, will be called LOTS
        //of times ever frame to handle different events.
        //We only need to Render our preview once when the GUI is being repainted!
        if (Event.current.type == EventType.Repaint) {
            //Tell the PRU to prepair itself - we pass along the
            //rect of the preview area so the PRU knows what size 
            //of a preview to render.
            m_PreviewRenderUtility.BeginPreview(r, background);

            //We draw our mesh manually - it is not attached to any 'gameobject' in the preview 'scene'.
            //The preview 'scene' only contains a camera and a light. We need to render things manually.
            //We pass along the mesh set on the mesh filter and the material set on the renderer
            m_PreviewRenderUtility.DrawMesh(m_TargetMeshFilter.sharedMesh, Matrix4x4.identity, m_TargetMeshRenderer.sharedMaterial, 0);

            //Tell the camera to actually render the preview.
            m_PreviewRenderUtility.camera.Render();

            //Now that we are done, we can end the preview. This method will spit out a Texture
            //The texture contains the image that was rendered by the preview utillity camera :)
            var result_render = m_PreviewRenderUtility.EndPreview();

            //If we omit the line bellow, then you wouldnt actually see anything in the preview!
            //The preview image is generated, but that was all done in our 'virtual' PreviewRenderUtility 'scene'.
            //We still need to draw something in the PreviewGUI area..!

            //So we draw the image that was generated into the preview GUI area, filling the entire area with this image.
            GUI.DrawTexture(r, result_render, ScaleMode.StretchToFill, false);
        }
    }

    private void OnDisable()
    {
        //Gotta clean up after yourself!
        if (m_PreviewRenderUtility != null) {
            m_PreviewRenderUtility.Cleanup();
        }
    }
}

preview of cube
As mentioned above, we don’t have to render a 3D preview in to the preview area. We can just render some GUI, and in certain situations we are not able to present our user with a preview even if we want to. As an example, if we remove the Mesh Renderer from our object, we will get lots of errors! Our preview GUI assumes the object has a Mesh Filter and a Mesh Renderer. Let’s do some error checking, and inform the user that their object requires a Mesh Renderer if they wish to preview it.

...

public override void OnPreviewGUI(Rect r, GUIStyle background)
{
    if (Event.current.type == EventType.Repaint) {
        //Mesh renderer missing?
        if (m_TargetMeshRenderer == null) {
            //EditorGUI.DropShadowLabel is used often in these preview areas - it 'fits' well.
            EditorGUI.DropShadowLabel(r, "Mesh Renderer Required");
        }
        else {
            m_PreviewRenderUtility.BeginPreview(r, background);

            m_PreviewRenderUtility.DrawMesh(m_TargetMeshFilter.sharedMesh, Matrix4x4.identity, m_TargetMeshRenderer.sharedMaterial, 0);
            m_PreviewRenderUtility.camera.Render();

            var result_render = m_PreviewRenderUtility.EndPreview();
            GUI.DrawTexture(r, result_render, ScaleMode.StretchToFill, false);
        }
    }
}
...

preview error handle
To polish up our preview, I’ve added some code to let you rotate the camera around. The code is pretty much identical to how Unity handles their camera rotation in the preview area, and is pretty straight forward.

I’ve also added a ‘reset camera’ button to the Preview header area! You can put whatever you like in that preview area by overriding OnPreviewSettings and rendering your own GUI. Again, check out the full code example bellow to see how it is done - it is super simple! 😃

Hopefully this has been somewhat helpful! Have fun with it! 😄
final preview

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MeshFilter))]
[CanEditMultipleObjects]
public class MeshFilterPreview : Editor
{
    private PreviewRenderUtility m_PreviewRenderUtility;
    private MeshFilter m_TargetMeshFilter;
    private MeshRenderer m_TargetMeshRenderer;

    private Vector2 m_Drag;

    private void ValidateData()
    {
        if (m_PreviewRenderUtility == null) {
            m_PreviewRenderUtility = new PreviewRenderUtility();

            m_PreviewRenderUtility.camera.transform.position = new Vector3(0, 0, -6);
            m_PreviewRenderUtility.camera.transform.rotation = Quaternion.identity;
        }

        m_TargetMeshFilter = target as MeshFilter;
        m_TargetMeshRenderer = m_TargetMeshFilter.GetComponent<MeshRenderer>();
    }

    public override bool HasPreviewGUI()
    {
        ValidateData();

        return true;
    }

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        m_Drag = Drag2D(m_Drag, r);

        if (Event.current.type == EventType.Repaint) {
            if (m_TargetMeshRenderer == null) {
                EditorGUI.DropShadowLabel(r, "Mesh Renderer Required");
            }
            else {
                m_PreviewRenderUtility.BeginPreview(r, background);

                m_PreviewRenderUtility.DrawMesh(m_TargetMeshFilter.sharedMesh, Matrix4x4.identity, m_TargetMeshRenderer.sharedMaterial, 0);

                m_PreviewRenderUtility.camera.transform.position = Vector2.zero;
                m_PreviewRenderUtility.camera.transform.rotation = Quaternion.Euler(new Vector3(-m_Drag.y, -m_Drag.x, 0));
                m_PreviewRenderUtility.camera.transform.position = m_PreviewRenderUtility.camera.transform.forward * -6f;
                m_PreviewRenderUtility.camera.Render();

                var result_render = m_PreviewRenderUtility.EndPreview();
                GUI.DrawTexture(r, result_render, ScaleMode.StretchToFill, false);
            }
        }
    }

    public override void OnPreviewSettings()
    {
        if (GUILayout.Button("Reset Camera", EditorStyles.whiteMiniLabel)) {
            m_Drag = Vector2.zero;
        }
    }

    private void OnDisable()
    {
        if (m_PreviewRenderUtility != null) {
            m_PreviewRenderUtility.Cleanup();
        }
    }

    public static Vector2 Drag2D(Vector2 scroll_pos, Rect position)
    {
        var control_ID = GUIUtility.GetControlID("Slider".GetHashCode(), FocusType.Passive);
        var current = Event.current;
        switch (current.GetTypeForControl(control_ID)) {
            case EventType.MouseDown:
                if (position.Contains(current.mousePosition) && position.width > 50f) {
                    GUIUtility.hotControl = control_ID;
                    current.Use();
                    EditorGUIUtility.SetWantsMouseJumping(1);
                }
                break;
            case EventType.MouseUp:
                if (GUIUtility.hotControl == control_ID) {
                    GUIUtility.hotControl = 0;
                }
                EditorGUIUtility.SetWantsMouseJumping(0);
                break;
            case EventType.MouseDrag:
                if (GUIUtility.hotControl == control_ID) {
                    scroll_pos -= current.delta * (float)((!current.shift) ? 1 : 3) / Mathf.Min(position.width, position.height) * 140f;
                    scroll_pos.y = Mathf.Clamp(scroll_pos.y, -90f, 90f);
                    current.Use();
                    GUI.changed = true;
                }
                break;
        }
        return scroll_pos;
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章