欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Unity编辑器】UVPreview扩展

程序员文章站 2022-07-14 11:33:26
...
最近为美术开发一个可以直接在unity中预览uv的编辑器,考虑到实用性,直接扩展了GameObject的Inspector,在GameObject的Preview中增加绘制UV的预览,支持查看模型光照贴图的UV布局:

效果:
UV预览:
【Unity编辑器】UVPreview扩展

光照贴图UV预览:
【Unity编辑器】UVPreview扩展

关键技术:几何着色器,C#反射

核心功能即UV的渲染,因此放到最前面讲:
第一步、uvmesh和uv绘制shader
最开始打算使用GL或Handles.DrawLine来绘制(注意Handles.DrawLine实际上就是封装了GL而已),但是对于面数高的模型效率不太高,所以考虑直接用集合着色器。可以通过mesh的uv信息生成一个用于渲染uv的mesh,也可以考虑直接在着色器中计算uv来渲染。
另外注意:由于几何着色器的特性需要target4.0,且PC上需要DX10以上才可以使用,如果当前Build Setting里并非PC平台,或没设置里没有勾选DX11,或这Graphics Emulation中勾选了OpenGl es2.0,则可能导致渲染uv的shader无法工作,不过unity为我们提供了一个Shader Tag:“ForceSupported”=”True”,加上这个标签后,除非你的显卡本身不支持几何着色器或不支持DX,否则不管当前的设置是什么都会强制支持。

UV渲染shader:
Tags{ "RenderType" = "Transparent" "Queue"="Transparent" "ForceSupported"="True" }
Pass
{
    cull off
    zwrite off
    blend srcalpha oneminussrcalpha
    CGPROGRAM
    #pragma vertex vert
    #pragma geometry geom
    #pragma fragment frag
    #pragma exclude_renderers opengl
    #pragma target 4.0
         
    #include "UnityCG.cginc"
 
    struct appdata
    {
        float4 vertex : POSITION;
    };
 
    struct v2g
    {
        float4 vertex : SV_POSITION;
        float4 worldPos : TEXCOORD0;
    };
 
    struct g2f {
        float4 vertex : SV_POSITION;
        float4 worldPos : TEXCOORD0;
    };
 
    float4 _Color;
    uniform float4x4 clipMatrix;
 
    v2g vert(appdata v)
    {
        v2g o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        return o;
    }
 
    [maxvertexcount(5)]
    void geom(triangle v2g i[3], inout LineStream<g2f> os)
    {
        g2f o;
 
        o.vertex = i[0].vertex;
        o.worldPos = i[0].worldPos;
        os.Append(o);
 
        o.vertex = i[1].vertex;
        o.worldPos = i[1].worldPos;
        os.Append(o);
 
        o.vertex = i[2].vertex;
        o.worldPos = i[2].worldPos;
        os.Append(o);
 
        os.RestartStrip();
 
        o.vertex = i[0].vertex;
        o.worldPos = i[0].worldPos;
        os.Append(o);
 
        o.vertex = i[2].vertex;
        o.worldPos = i[2].worldPos;
        os.Append(o);
         
    }
 
    fixed4 frag(g2f i) : SV_Target
    {  
        float4 clipPos = mul(clipMatrix, i.worldPos);
        if (clipPos.x > 1 || clipPos.x < 0 || clipPos.y>1 || clipPos.y < 0)
            discard;
        return _Color;
    }
    ENDCG
}

第二步:绘制UV:
这一步我首先根据mesh的uv信息计算并生成一个uvmesh,即一个直接将uv坐标作为顶点坐标的mesh(当然这一步可以直接在shader中完成)。
然后直接使用Graphics.DrawMeshNow来绘制uvmesh,这里有一个难点,GUI绘制传递的是一个Rect,而绘制mesh需要一个矩阵,并且在uvpreview编辑器中,我需要保证uv托盘始终是长宽比为1,而且需要可以缩放的,因此这里就涉及到一个将Rect转换为2D GUI绘制矩阵的问题,代码如下:

private void DrawUVMesh(Rect rect, int uvID, int pass)
   {
 
        Matrix4x4 matrix = default(Matrix4x4);
        //计算并设置裁剪矩阵
        m_BoardLineMaterial.SetMatrix("clipMatrix", GetGUIClipMatrix(rect));
 
        //非光照贴图布局模式下直接计算绘制矩阵
        if (!m_LightMapLayoutMode)
            matrix = RefreshMatrix(rect);
        for (int i = 0; i < m_UVDatas.Count; i++)
        {
            if (m_UVDatas[i].disable)
                continue;
            if (m_UVDatas[i].target == null)
                continue;
            m_BoardLineMaterial.SetPass(pass);
            Mesh mesh = null;
            if (m_LightMapLayoutMode)
            {
                //光照贴图布局模式下需要根据每个Renderer的LightMapScaleOffset来计算绘制矩阵
                Renderer renderer = m_UVDatas[i].target.GetComponent<Renderer>();
                if (renderer.lightmapIndex != LightMapIndex)
                    continue;
                Vector4 lmST = renderer.lightmapScaleOffset;
 
                matrix =
                    RefreshMatrix(rect, lmST.z, lmST.w, lmST.x, lmST.y);
                mesh = m_UVDatas[i].uvMeshs[1];
                if (mesh == null)
                    mesh = m_UVDatas[i].uvMeshs[0];
            }
            else
            {
                mesh = m_UVDatas[i].uvMeshs[uvID];
            }
            if (mesh)
                Graphics.DrawMeshNow(mesh, matrix);
        }
    }

矩阵转换相关函数:
/// <summary>
    /// 绘制矩阵计算
    /// </summary>
    /// <param name="r"></param>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="w"></param>
    /// <param name="h"></param>
    /// <returns></returns>
    private Matrix4x4 RefreshMatrix(Rect r, float x = 0, float y = 0, float w = 1, float h = 1)
    {
        //该矩阵会在绘制区域r发生变化时更新,并根据r的宽高比自适应绘制区域
        r.x = r.x - (uvPanelScale.x - 1) * r.width / 2;
        r.y = r.y - (uvPanelScale.y - 1) * r.height / 2;
        r.x += uvPanelPosition.x;
        r.y += uvPanelPosition.y;
        r.width *= uvPanelScale.x;
        r.height *= uvPanelScale.y;
        return GetGUISquareMatrix(r, x, y, w, h);
    }
 
    private static Matrix4x4 GetGUISquareMatrix(Rect r, float x = 0, float y = 0, float w = 1, float h = 1)
    {
        //该矩阵会在绘制区域r发生变化时更新,并根据r的宽高比自适应绘制区域
        Matrix4x4 m_Matrix = new Matrix4x4();
        float aspect = r.width / r.height;
        if (aspect > 1)
        {
            m_Matrix.m00 = r.height * w;
            m_Matrix.m03 = r.x + r.width / 2 - r.height / 2 + r.height * x;
            m_Matrix.m11 = -r.height * h;
            m_Matrix.m13 = r.y + r.height - y * r.height;
        }
        else
        {
            m_Matrix.m00 = r.width * w;
            m_Matrix.m03 = r.x + x * r.width;
            m_Matrix.m11 = -r.width * h;
            m_Matrix.m13 = r.y + r.height / 2 + r.width / 2 - y * r.width;
        }
        m_Matrix.m33 = 1;
        return m_Matrix;
    }
 
    /// <summary>
    /// 通过GUI绘制区域获取GUI裁剪矩阵计算
    /// </summary>
    /// <param name="r">绘制区域</param>
    /// <returns></returns>
    public static Matrix4x4 GetGUIClipMatrix(Rect r)
    {
        //fx = (x-r.x)/r.width;
        //fy = (y-r.y)/r.height;
        Matrix4x4 matrix = new Matrix4x4();
        matrix.m00 = 1 / r.width;
        matrix.m03 = -r.x / r.width;
        matrix.m11 = 1 / r.height;
        matrix.m13 = -r.y / r.height;
        matrix.m33 = 1;
        return matrix;
    }


其中GetGUIClipMatrix的作用是计算一个GUI裁剪矩阵,并传递到Shader中,保证uv超出绘制矩形的区域可以裁剪掉。

第三步、扩展GameObjectInspector
我们实现某些代码的时候需要扩展其Inspector界面,可以通过继承Editor类来实现,但是注意到unity本身已经为GameObject实现了Inspector类:GameObjectInspector,但该类是内部类,我们无法扩展:
【Unity编辑器】UVPreview扩展

这时候就要用上我们CSharp强大的反射功能,来实现不继承GameObjectInspector的情况下扩展默认GameObjectInspector了:
实际上这里真正需要用到反射的部分只有两个:
反射1.从UnityEditor程序集中反射获取到UnityEditor.GameObjectInspector类,并使用该类来构造GameObjectInspector:
System.Type gameObjectorInspectorType = typeof (Editor).Assembly.GetType("UnityEditor.GameObjectInspector");
m_GameObjectInspector = Editor.CreateEditor(target, gameObjectorInspectorType);

之后我们重载Editor类的大部分方法,由于我们只扩展DrawPreview方法,所以其它重载的方法直接调用刚刚获取到的GameObjectInspector的默认行为就行了。
这里唯一需要注意的是OnHeaderGUI方法:
【Unity编辑器】UVPreview扩展

可以看到OnHeaderGUI是个受保护的方法,因此即便我们重载了它,也没办法调用GameObjectInspector的默认OnHeaderGUI,这时就必须再次用到反射了。

反射2.从GameObjectInspector类中反射OnHeaderGUI方法:

void OnEnable()
{
     System.Type gameObjectorInspectorType = typeof (Editor).Assembly.GetType("UnityEditor.GameObjectInspector"); 
     m_OnHeaderGUI = gameObjectorInspectorType.GetMethod("OnHeaderGUI", BindingFlags.NonPublic | BindingFlags.Instance);
     m_GameObjectInspector = Editor.CreateEditor(target, gameObjectorInspectorType);
}
 
protected override void OnHeaderGUI()
{
    if (m_OnHeaderGUI != null)
    {
        m_OnHeaderGUI.Invoke(m_GameObjectInspector, null);
    }
}

这样就可以实现在在不继承GameObjectInspector的情况下来扩展GameObjectInspector的功能了。