GUIClip在IMGUI中的作用
简介
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();
}
}
可以看到LabelAtOrigin(GUIClip.topmostRect.ToString());
打印了GUIClip
的Rect的数值和显示的不符,
这是因为GUIClip
存储的时候是不考虑matrix的。但是显示的时候又应用了matrix。
/// 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);
实战训练
在没有缩放的情况下,我们将鼠标悬浮在NodeCanvas
的节点上,鼠标会变为MouseCursor.Link
手指的样式,这是因为在DrawNodeWindow
函数中调用了EditorGUIUtility.AddCursorRect
。
if (zoomFactor == 1f)
{
EditorGUIUtility.AddCursorRect(new Rect(node.rect.x, node.rect.y, node.rect.width, node.rect.height), MouseCursor.Link);
}
但是这个鼠标样式的修改只会发生在zoomfactor
为1的时候,如果我们去掉上面代码中的if
判断,我们会发现,如果我们缩小画布, CursorRect
会发生错位。
这是因为EditorGUIUtility.AddCursorRect
调用了GUIClip.UnClip(Rect)
。而这个函数的底层代码其实是有问题的,它没有将GUI.matrix
考虑进去。
public static void AddCursorRect(Rect position, MouseCursor mouse, int controlID)
{
if (Event.current.type == EventType.Repaint)
{
Rect rect = GUIClip.Unclip(position);
Rect topmostRect = GUIClip.topmostRect;
Rect r = Rect.MinMaxRect(Mathf.Max(rect.x, topmostRect.x), Mathf.Max(rect.y, topmostRect.y), Mathf.Min(rect.xMax, topmostRect.xMax), Mathf.Min(rect.yMax, topmostRect.yMax));
if (!(r.width <= 0f) && !(r.height <= 0f))
{
Internal_AddCursorRect(r, mouse, controlID);
}
}
}
//没有考虑m_Matrixd
Rectf GUIClipState::Unclip (const Rectf& rect)
{
if (!m_GUIClips.empty())
{
GUIClip& topmost = m_GUIClips.back();
return Rectf (rect.x + topmost.scrollOffset.x + topmost.physicalRect.x,
rect.y + topmost.scrollOffset.y + topmost.physicalRect.y,
rect.width, rect.height);
}
else
{
return Rectf (0,0,0,0);
}
}
unity
的GUI.matrix
是作用于绝对坐标的,这里我们只需将Rect
的点min和点max,都用UnClip
函数将其转化为绝对坐标即可。
public static void AddCursorRect(Rect position, MouseCursor mouse)
{
if (Event.current.type == EventType.Repaint)
{
// unclip the local position
Vector2 min = UnClip(position.min, GraphEditorWindow.current.ScrollOffset);
Vector2 max = UnClip(position.max, GraphEditorWindow.current.ScrollOffset);
Rect rect = Rect.MinMaxRect(min.x, min.y, max.x, max.y);
//Rect topmostRect = GUIClip.topmostRect;
//Rect r = Rect.MinMaxRect(Mathf.Max(rect.x, topmostRect.x), Mathf.Max(rect.y, topmostRect.y), Mathf.Min(rect.xMax, topmostRect.xMax), Mathf.Min(rect.yMax, topmostRect.yMax));
//if (!(r.width <= 0f) && !(r.height <= 0f))
{
Internal_AddCursorRect(rect, mouse, 0);
}
}
}
//带matrix的UnClip源码
Vector2f GUIClipState::Unclip (const Vector2f& pos)
{
if (!m_GUIClips.empty())
{
GUIClip& topmost = m_GUIClips.back();
Vector3f res;
m_Matrix.PerspectiveMultiplyPoint3 (Vector3f (pos.x, pos.y, 0.0F), res);
return Vector2f(res.x, res.y) + topmost.scrollOffset + Vector2f (topmost.physicalRect.x, topmost.physicalRect.y);
}
else
{
return Vector2f (0,0);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)