目录
简介
Unity中的IMGUI
是一个独立于ugui的UI系统。IMGUI
是事件(消息)驱动的UI系统,主要用于编写开发工具。
Unity官方目前并无GUIClip
的相关文档,本篇文章的主要目的是描述GUIClip
类在 IMGUI
中的作用,给有需要的同学提供一些学习资料。
IMGUI
IMGUI
是WinAPI和图形API(DirectX, OpenGL等)的简单拼装。
- 在
OnGUI
函数回调中,我们需要根据不同的事件类型,做出不同的处理。由于需要围绕事件写代码,所以IMGUI
是事件驱动的。 - 我们可以直接使用
GUI
,GUILayout
类中写好的控件。GUIUtility
和EditorGUIUtility
提供了一些功能函数。
IMGUI
控件的渲染其实存在两个阶段,一个是Setup
,另一个是Display
。
- 在
Setup
阶段,我们需要设置一些参数用于坐标系变换。GUIClip
和GUI.matrix
涉及坐标系的变化,对应到DirectX就是IDirect3DDevice9::SetTransform
- 在
Display
阶段,我们需要调用Draw函数来渲染图形。GUIStyle
负责图像的渲染,对应到DirectX就是IDirect3DDevice9::DrawIndexedPrimitive
GUIClip
简单介绍了IMGUI
,这里我们进入正题。就像上面说的,GUIClip
在IMGUI
系统中的作用涉及坐标系的变化。GUIClip
在底层以栈Stack的形式保存。每次推入一个新的GUIClip
,我们就进入了一个新的坐标系。
Push Pop Count
我们可以使用GUIClip.Internal_GetCount()
获得push进去的GUIClip
的个数,以及使用GUIClip.Push
推入一个新的GUIClip
。
如果遇到"internal函数无法调用"的错误提示,可以参考《Cecil修改UnityDll,不使用反射就能调用internal的函数》
private void OnGUI()
{
Debug.Log($"OnGUIStart: GUIClip.Count={GUIClip.Internal_GetCount()}");
var rect = new Rect(10,10,100,100);
GUIClip.Push(rect, default, default, false);
Debug.Log($"AfterPush: GUIClip.Count={GUIClip.Internal_GetCount()}");
GUIClip.Pop();
Debug.Log($"AfterPop: GUIClip.Count={GUIClip.Internal_GetCount()}");
}
//OnGUIStart: GUIClip.Count=1
//AfterPush: GUIClip.Count=2
//AfterPop: GUIClip.Count=1
运行代码,我们可以看到在OnGUI
函数开始的时候,就已经有一个GUIClip
被push进去。接着我们调用GUIClip.Push
,日志打印的GUICLip
个数会加1变成2。对应的,如果我们pop出最近的GUIClip
,打印的个数会变为1。
局部坐标
上文提到了,每次推入一个新的GUIClip
,我们就进入了一个新的坐标系。IMGUI
中的坐标基本上都是相对当前坐标系的局部坐标。
比如,我们在当前坐标系中调用GUIStyle.Draw
和在push一个新的GUIClip
后调用GUIStyle.Draw
,它们的位置是不同的。
StyleDraw中Rect点的位置
private void OnGUI()
{
var currentEvent = Event.current.mousePosition;
var localRect = new Rect(50,50,200,200);
//在原坐标系中 渲染 新的GUIClip的Rect
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
GUIClip.Push(localRect, default, default, false);
//在新坐标系中 渲染 相同的Rect
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
GUIClip.Pop();
}
虽然代码中都是调用GUIStyle.Draw
渲染Rect(50,50,200,200) 这个长方形位置的图片,但由于Rect是局部坐标,并且GUIClip
的绝对位置不同,因此它们渲染的位置不同。
鼠标位置
除了GUIStyle.Draw
传入的Rect
是局部坐标。Event.mousePositin
返回的也是局部坐标。
如果我们修改上面的代码,让其在当前坐标系的(0,0)打印鼠标的位置(红点标记),我们可以发现鼠标位置在不同GUIClip
中的局部坐标也是不同的。
private void OnGUI()
{
var currentEvent = Event.current.mousePosition;
var localRect = new Rect(50,50,200,200);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelMousePos();
GUIClip.Push(localRect, default, default, false);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelMousePos();
GUIClip.Pop();
}
private void LabelMousePos()
{
string content = $"{Event.current.mousePosition}";
var size = GUI.skin.label.CalcSize(GUIContent.Temp(content));
GUI.Label(new Rect(0, 0, size.x, size.y), $"{Event.current.mousePosition}");
}
绝对坐标
既然有局部坐标,那么就有绝对坐标。绝对坐标通俗点说就是相对窗口左上角的坐标。
对于当前GUIClip
的Rect
,我们可以使用GUIClip.topmostRect
获取的绝对坐标。
注意
GUIClip.GetTopRect()
返回的是当前GUIClip
在上一个GUIClip
中的相对位置。
我们可以使用下面的代码,在当前GUIClip
原点的位置显示当前GUICLip
的绝对坐标。
private void OnGUI()
{
var currentEvent = Event.current.mousePosition;
var localRect = new Rect(50,50,200,200);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin(GUIClip.topmostRect.ToString());
GUIClip.Push(localRect, default, default, false);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin(GUIClip.topmostRect.ToString());
Debug.Log(GUIClip.GetTopRect());
GUIClip.Pop();
}
private void LabelAtOrigin(string labelContent)
{
var size = GUI.skin.label.CalcSize(GUIContent.Temp(labelContent));
GUI.Label(new Rect(0, 0, size.x, size.y), labelContent);
}
可以发现刚进入OnGUI
函数时就存在的GUIClip
的绝对坐标的位置是(0,21),这是因为EditorWindow
实际上只是DockArea(GUIView)
的一个pane
。在WinAPI中注册的窗口属于GUIView
这个类,DockArea
派生自GUIView
,可以有多个EditorWindow
。DockArea
调用OldOnGUI
函数时,会调用GUIView.BeginOffsetArea
推入一个GUIClip
,之后才会接着调用当前EditorWindow
的OnGUI
函数。
ScrollOffset对局部坐标的影响
我们修改代码,对push的GUIClip
添加ScrollOffset
参数。
我们在第一个GUIClip
的局部坐标原点显示ScrollOffset
参数的值,在第二个的GUIClip
的局部坐标原点显示第二个GUIClip
的绝对坐标。
以及在当前GUIClip
的mousePosition
位置打印mousePosition
。
private void OnGUI()
{
var localRect = new Rect(50,50,200,200);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin("scrollOffset="+scrollOffset.ToString());
GUIClip.Push(localRect, scrollOffset, default, false);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin(GUIClip.topmostRect.ToString());
LabelAt(Event.current.mousePosition, $"{Event.current.mousePosition}");
GUIClip.Pop();
if (Event.current.isScrollWheel)
{
var delta = Event.current.delta; //向下滚动滚轮, delay.y>0
if (Event.current.alt)
scrollOffset.x += delta.y; //scrollOffset.y<0时ui往上移动
else
scrollOffset.y -= delta.y; //scrollOffset.y<0时ui往上移动
Event.current.Use();
}
}
private void LabelAtOrigin(string labelContent)
{
LabelAt(Vector2.zero, labelContent);
}
private void LabelAt(Vector2 pos, string labelContent)
{
var size = GUI.skin.label.CalcSize(GUIContent.Temp(labelContent));
GUI.Label(new Rect(pos.x, pos.y, size.x, size.y), labelContent);
}
可以看到随着scrollOffset
的变化, 第二个GUIClip
的局部坐标的位置都随之发生了变化。
但是GUIClip
的绝对坐标并没有因为scrollOffset
的变化而发生变化。
局部坐标和绝对坐标的相互转化
上一小节,我们可以观察到虽然鼠标没有挪动,但是随着scrollOffset
的变化,鼠标局部坐标的数值也发生变化,从而得出scrollOffset
可以影响到局部坐标的数值。
除此之外,GUI.matrix
也可以影响到绝对坐标和局部坐标的相互转化。
这里给出示例代码以及对应的源码
private void OnGUI()
{
var localRect = new Rect(50,50,200,200);
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin("scale=" + scale.ToString());
GUIClip.Push(localRect, scrollOffset, default, false);
GUI.matrix = Matrix4x4.Scale(new Vector3(scale, scale, 1));
if(Event.current.type == EventType.Repaint)
GUI.skin.window.Draw(localRect, GUIContent.none, 0);
LabelAtOrigin(GUIClip.topmostRect.ToString());
LabelAt(Event.current.mousePosition, $"{Event.current.mousePosition}");
GUI.matrix = Matrix4x4.identity;
GUIClip.Pop();
if (Event.current.isScrollWheel)
{
var delta = Event.current.delta; //向下滚动滚轮, delay.y>0
scale += delta.y *0.01f;
Event.current.Use();
}
}
/// Clips /absolutePos/ to drawing coordinates
Vector2 Clip(Vector2 absolutePos, Vector2 scrollOffset) // m_AbsoluteMousePosition
{
if (GUIClip.Internal_GetCount() == 0)
{
return default;
}
var inverseMatrix = Matrix4x4.Inverse(GUIClip.GetMatrix());
Vector2 transformedPoint = inverseMatrix.MultiplyPoint(absolutePos);
Vector2 result = transformedPoint - scrollOffset - GUIClip.topmostRect.position;
return result;
}
//Unity的 UnClip函数的实现
Vector2 UnClip_F__M_(Vector2 pos, Vector2 scrollOffset)
{
if (GUIClip.Internal_GetCount() == 0)
{
return default;
}
var matrix = GUIClip.GetMatrix();
Vector2 transformedPoint = matrix.MultiplyPoint(new Vector3(pos.x, pos.y, 0.0F));
return transformedPoint + scrollOffset + GUIClip.topmostRect.position;
}
裁剪
GUIClip
的裁剪分成几部分,
- 在push进一个新的
GUIClip
的时候,底层函数会保证新的GUIClip
的4个点的绝对坐标不超过当前的GUIClip
。 GUIStyle
调用Draw
的时候,会将局部坐标在m_VisibleRect 之外的点都裁剪掉。- 渲染管道的Clipping裁剪阶段会将位于
View Volume
之外的点裁剪掉。
m_VisibleRect = Rectf (-topmost.scrollOffset.x, -topmost.scrollOffset.y, topmost.physicalRect.width, topmost.physicalRect.height);
参考链接
Immediate Mode GUI – Theory and Example
标签:IMGUI,localRect,GUI,current,坐标,GUIClip,Event,作用 From: https://www.cnblogs.com/dewxin/p/18685680