GUIClip在IMGUI中的作用

简介

Unity中的IMGUI是一个独立于ugui的UI系统。IMGUI事件(消息)驱动的UI系统,主要用于编写开发工具。
Unity官方目前并无GUIClip的相关文档,本篇文章的主要目的是描述GUIClip类在 IMGUI中的作用,给有需要的同学提供一些学习资料。

IMGUI

IMGUI是WinAPI和图形API(DirectX, OpenGL等)的简单拼装。

  • OnGUI函数回调中,我们需要根据不同的事件类型,做出不同的处理。由于需要围绕事件写代码,所以IMGUI事件驱动的。
  • 我们可以直接使用GUI,GUILayout类中写好的控件。GUIUtilityEditorGUIUtility提供了一些功能函数。

IMGUI控件的渲染其实存在两个阶段,一个是Setup,另一个是Display

  • Setup阶段,我们需要设置一些参数用于坐标系变换。GUIClipGUI.matrix涉及坐标系的变化,对应到DirectX就是IDirect3DDevice9::SetTransform
  • Display阶段,我们需要调用Draw函数来渲染图形。GUIStyle负责图像的渲染,对应到DirectX就是IDirect3DDevice9::DrawIndexedPrimitive

GUIClip

简单介绍了IMGUI,这里我们进入正题。就像上面说的,GUIClipIMGUI系统中的作用涉及坐标系的变化。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}");
}

绝对坐标

既然有局部坐标,那么就有绝对坐标。绝对坐标通俗点说就是相对窗口左上角的坐标
对于当前GUIClipRect,我们可以使用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,可以有多个EditorWindowDockArea调用OldOnGUI函数时,会调用GUIView.BeginOffsetArea推入一个GUIClip,之后才会接着调用当前EditorWindowOnGUI函数。

ScrollOffset对局部坐标的影响

我们修改代码,对push的GUIClip添加ScrollOffset参数。
我们在第一个GUIClip的局部坐标原点显示ScrollOffset参数的值,在第二个的GUIClip的局部坐标原点显示第二个GUIClip的绝对坐标。
以及在当前GUIClipmousePosition位置打印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);
	}
}

unityGUI.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);
	}
}

参考链接

Immediate Mode GUI – Theory and Example

IMGUI crash course

posted @   dewxin  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示