【Unity编辑器】UVPreview扩展
程序员文章站
2022-07-14 11:33:26
...
最近为美术开发一个可以直接在unity中预览uv的编辑器,考虑到实用性,直接扩展了GameObject的Inspector,在GameObject的Preview中增加绘制UV的预览,支持查看模型光照贴图的UV布局:
效果:
UV预览:
光照贴图UV预览:
关键技术:几何着色器,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,但该类是内部类,我们无法扩展:
这时候就要用上我们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方法:
可以看到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);
}
}