Unity怎么判断图形(Graphic)是否被遮挡
起因:
最近领了个需求,需要给项目的弱引导增加个功能,判断它是否被其他UI遮挡住,如果被遮挡了就需要实时将它隐藏,遮挡结束则恢复显示,这个需求乍一看似乎有点不太变态,但细细想想似乎还是能够做到,以下将我的经验分享一下。
遮挡判断:
要想解决这个问题,最重要的是要知道一个ui怎么算是被遮挡了。
-
第一个思路是,我认为一张图像的组成是由贴图采样得到的像素点中所有RGBA值中所有alpha值大于0的部分, 我们需要用最后输出到屏幕上的贴图和原贴图做逐个像素的对比,如果有不一致的地方则认为被遮挡了。但这个明显实现起来过于耗,而且结果不一定尽人意,假如仅有一个像素点被覆盖了,我们也认为它被遮挡了是比较离谱的,虽然我们可以定义个百分比的阙值,但毕竟还是对需求妥协了,所以不如换个更简单的思路。
-
第二个思路,判断网格中所有顶点的采样得到像素值是否被其他颜色覆盖了,这样确实简化了不少,但是要想判断像素值,势必要开启所有图集资源的读写,仅仅是为了这个需求显然是不合理。
-
第三个思路,所以我们简化一步到位,直接判断ui的中心点,并且不判断它的像素值,而是通过一条射线找到该点上的所有graph组件,剔除掉透明的组件并排序好层级关系后,判断该ui是否被遮挡。
在遮挡这,我们即可采取第三个思路来做处理,还是额外说明一点,如果graph是目标子节点,也可以认为它并没遮挡目标,因为它本身就是目标节点的一部分。
剔除看不见的UI
我们找到了该点上的所有UI,自然要剔除掉那些看不见的UI,我大致总结下有哪些因素会导致指向目标我们“看不见”
-
目标的gameObject.activeInHierarchy为false
-
目标graph的alpha值为0
-
目标上有CanvasGroup组件或其父对象上有CanvasGroup组件,且CanvasGroup组件的alpha值为0
-
目标被Mask组件或者RectMask2D组件裁剪
-
目标的canvasRender组件的cull属性为false(cull:指示是否忽略该渲染器发射的几何形状)
-
目标的canvasRender组件的absoluteDepth属性小于0(absoluteDepth:渲染器相对于根画布的深度)
-
目标不在屏幕范围内
思路总结
综上所述,我们大致就能得出一个做法,取得“指向目标”的屏幕坐标,找到屏幕上所有包含该坐标的UI,剔除掉看不见的ui,按照从低到高的位置排好序,如果最高层UI是“指向目标”的话,则代表没有任何遮挡。
接下来,我将通过代码实现下我们上面所诉的每个条件。
获得UI中心点上所有的UI
var rectTransform = targetGraphic.transform as RectTransform;
var worldPos = rectTransform.TransformPoint(rectTransform.rect.center);
var screenPos = Camera.main.WorldToScreenPoint(worldPos); // 用自己项目的ui摄像机来做转换,我这里demo演示用的Camera.main
// 简化处理,拿到场景中所有的Canvas
List<Canvas> canvases = GameObject.FindObjectsOfType<Canvas>().ToList();
for (int i = 0, icnt = canvases.Count; i < icnt; i++)
{
// 获得该canvas画布下所有注册进去的Graphic
var canvas = canvases[i];
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
}
判断ui是否被Mask遮挡住
先总结下Mask的实现原理,Mask激活时,会修改MaskGraphic的材质,该材质会对每个像素点进行标记,将标记结果存入模板缓存中,当子级UI渲染中,如果未通过模板缓冲区的测试,则会丢弃到未通过的像素。
一言难尽,先放代码,后面另开篇文章总结怎么判断ui是否被Mask和RectMask2D遮罩,它的原理和它能否被点击到相关
/// <summary>
/// 是否有被Mask组件或者RectMask2D裁剪,可以假设Rect中心点不显示了,就认为被裁剪了(当然也可以4个边点,我这里从简计算)
/// </summary>
/// <param name="graphic"></param>
/// <returns></returns>
private bool IsMaskCull(Image graphic, Camera eventCamera)
{
if (graphic == null)
{
return false;
}
var t = graphic.transform;
var worldPos = t.TransformPoint((t as RectTransform).rect.center);
var screenPos = eventCamera.WorldToScreenPoint(worldPos);
// 如果有Canvas,且Canvas的overrideSorting属性为true,则上层不再影响它们
bool continueTraversal = true;
bool valid = true;
List<Component> components = new List<Component>();
while (t != null)
{
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
{
continueTraversal = true;
}
var mask = components[i] as Mask;
var rectMask2D = components[i] as RectMask2D;
if (mask == null && rectMask2D == null)
{
continue;
}
var filter = components[i] as ICanvasRaycastFilter; // 很有意思一点,对于Mask和RectMask2D 点不到跟看不到一个道理
valid = filter.IsRaycastLocationValid(screenPos, eventCamera);
if (!valid)
{
return true;
}
}
t = continueTraversal ? t.parent : null;
}
return false;
}
判断canvasGroup是否alpha为0
/// <summary>
/// 返回节点或其父节点最近邻的CanvasGroup的alpah是否为0
/// </summary>
/// <returns></returns>
private bool IsCanvasGroupAlphaZero(Graphic graphic)
{
List<Component> components = new List<Component>();
var t = graphic.transform;
bool hasCanvasGroup = false;
float alpha = 1f;
while (t != null)
{
if (hasCanvasGroup)
{
break;
}
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
// 拿到最近邻的alpha
var group = components[i] as CanvasGroup;
if (group == null || !group.enabled)
{
continue;
}
alpha = group.alpha;
hasCanvasGroup = true;
break;
}
t = t.parent;
}
return alpha == 0;
}
判断grapha是否包含目标中心点
// 如果aabb包围盒不包含点击点则剔除
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, eventPosition, Camera.main, graphic.raycastPadding))
{
continue;
}
判断是否忽略canvasRender渲染
// 如果忽略canvasRender的渲染,或graphic深度不正常(graphic.depth == graphic.canvasRenderer.absoluteDepth)则剔除
if (graphic.canvasRenderer.cull || graphic.depth == -1)
{
continue;
}
判断材质是否透明
// 如果材质透明则剔除
if (graphic.color.a == 0f)
{
continue;
}
判断Canvs上的点击点是否在屏幕范围内
考虑多显示器的情况,获得鼠标在某个显示器上真正的坐标,判断鼠标有没有超出该显示器的长宽范围
/// <summary>
/// 判断Graphic是否在屏幕范围内,仅讨论canvas renderMode是Camera的情况
/// </summary>
/// <param name="graphic"></param>
/// <param name="eventCamera"></param>
/// <returns></returns>
private bool InScreen(Canvas canvas, PointerEventData eventData, Camera eventCamera)
{
// 该情况过于复杂,大概说下流程
// 1. 首先调用Display.RelativeMouseAt(eventData.position)方法来获取事件相对于屏幕的返回
// 2. 如果返回的位置不是零向量(Vector3.zero),则表示事件确实发生在某显示器上
// 3. 返回的事件位置(eventPosition)的z会作为显示器索引
// 4. 如果返回零向量,则意为多显示器系统在该平台上不受支持,在这种情况下,会假定认为是事件在当前显示器上发生
// 5. 最终根据显示器索引获得显示器分辨率(Display.displays[index])
// 6. 判断鼠标位置是否有超出屏幕分辨率
int displayIndex = canvas.targetDisplay;
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int) eventPosition.z;
if (eventDisplayIndex != displayIndex)
{
return false;
}
}
else
{
eventPosition = eventData.position;
}
Vector2 pos;
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || eventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
{
pos = eventCamera.ScreenToViewportPoint(eventPosition);
}
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
{
return false;
}
return true;
}
源码展示
最后展示一份完整的源码,为了对各个原因做区分,没有考虑到性能问题,有用到的可以自己优化。如果有更多意见的,欢迎一起交流哈
完整源码
using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UITopmostChecker : MonoBehaviour
{
public Graphic targetGraphic;
[NonSerialized]
[ShowInInspector]
public bool block; // 输出ui是否被遮挡了
private void Update()
{
if (targetGraphic == null || !targetGraphic.isActiveAndEnabled)
{
block = true;
return;
}
var results = GetTargetCenterPointAllGraphic();
if (results.Count == 0)
{
block = true;
return;
}
for (int i = results.Count - 1; i >= 0; i--)
{
var graphic = results[i];
if (graphic == targetGraphic)
{
block = false;
return;
}
// 子节点不认为会遮挡父节点
if (CheckParentIsTarget(targetGraphic, graphic))
{
continue;
}
break;
}
block = true;
}
private void OnDrawGizmos()
{
var dir = transform.rotation * Vector3.forward;
// Gizmos.DrawRay();
}
/// <summary>
/// 检查grahic的父节点中是否包含target
/// </summary>
/// <param name="target"></param>
/// <param name="from"></param>
/// <returns></returns>
private bool CheckParentIsTarget(Graphic target, Graphic from)
{
if (target == null || from == null)
{
return false;
}
Transform parent = from.transform;
while (parent != null)
{
var graphic = parent.GetComponent<Graphic>();
if (target == graphic)
{
return true;
}
parent = parent.parent;
}
return false;
}
/// <summary>
/// 返回ui中心点上所有ui,层级从低到高返回
/// </summary>
/// <returns></returns>
private List<Graphic> GetTargetCenterPointAllGraphic()
{
List<Graphic> resultGraphic = new List<Graphic>();
var rectTransform = targetGraphic.transform as RectTransform;
var worldPos = rectTransform.TransformPoint(rectTransform.rect.center);
var screenPos = Camera.main.WorldToScreenPoint(worldPos); // 用自己项目的ui摄像机来做转换,我这里demo演示用的Camera.main
// 判断下是否在屏幕内
PointerEventData pointerData = new PointerEventData(UnityEngine.EventSystems.EventSystem.current);
pointerData.position = screenPos;
// 简化处理,拿到场景中所有的Canvas
List<Canvas> canvases = GameObject.FindObjectsOfType<Canvas>().ToList();
for (int i = 0, icnt = canvases.Count; i < icnt; i++)
{
var canvas = canvases[i];
if (!canvas.isActiveAndEnabled)
{
continue;
}
// 在该Canvas的点击不再屏幕内的话,不用考虑该Canvas
if (!InScreen(canvas, pointerData, Camera.main))
{
continue;
}
// 获得该canvas画布下所有注册进去的Graphic
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
{
continue;
}
// 取得screenPos下所有可见ui
var sortGraphics = GetValidGraphic(canvasGraphics, screenPos);
if (sortGraphics == null)
{
continue;
}
for (int j = 0, jcnt = sortGraphics.Count; j < jcnt; j++)
{
var graphic = sortGraphics[j];
resultGraphic.Add(graphic);
}
}
resultGraphic.Sort((a, b) => a.depth.CompareTo(b.depth));
return resultGraphic;
}
/// <summary>
/// 返回鼠标点这条射线上所有ui,剔除掉不显示的ui
/// </summary>
/// <param name="canvasGraphics">指定Canvas</param>
/// <param name="eventPosition">鼠标点</param>
/// <returns></returns>
private List<Graphic> GetValidGraphic(IList<Graphic> canvasGraphics, Vector2 eventPosition)
{
List<Graphic> sortGraphics = new List<Graphic>();
for (int i = 0, icnt = canvasGraphics.Count; i < icnt; i++)
{
Graphic graphic = canvasGraphics[i];
// 如果忽略canvasRender的渲染,或graphic深度不正常(graphic.depth == graphic.canvasRenderer.absoluteDepth)则剔除
if (graphic.canvasRenderer.cull || graphic.depth == -1)
{
continue;
}
// 如果材质透明则剔除
if (graphic.color.a == 0f)
{
continue;
}
// 如果CanvasGroup透明则剔除
if (IsCanvasGroupAlphaZero(graphic))
{
continue;
}
// 如果aabb包围盒不包含点击点则剔除
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, eventPosition, Camera.main, graphic.raycastPadding))
{
continue;
}
if (IsMaskCull(graphic as Image, Camera.main))
{
continue;
}
sortGraphics.Add(graphic);
}
sortGraphics.Sort((a, b) => a.depth.CompareTo(b.depth));
return sortGraphics;
}
/// <summary>
/// 返回节点或其父节点最近邻的CanvasGroup的alpah是否为0
/// </summary>
/// <returns></returns>
private bool IsCanvasGroupAlphaZero(Graphic graphic)
{
List<Component> components = new List<Component>();
var t = graphic.transform;
bool hasCanvasGroup = false;
float alpha = 1f;
while (t != null)
{
if (hasCanvasGroup)
{
break;
}
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
// 拿到最近邻的alpha
var group = components[i] as CanvasGroup;
if (group == null || !group.enabled)
{
continue;
}
alpha = group.alpha;
hasCanvasGroup = true;
break;
}
t = t.parent;
}
return alpha == 0;
}
/// <summary>
/// 是否有被Mask组件或者RectMask2D裁剪,可以假设Rect中心点不显示了,就认为被裁剪了(当然也可以4个边点,我这里从简计算)
/// </summary>
/// <param name="graphic"></param>
/// <returns></returns>
private bool IsMaskCull(Image graphic, Camera eventCamera)
{
if (graphic == null)
{
return false;
}
var t = graphic.transform;
var worldPos = t.TransformPoint((t as RectTransform).rect.center);
var screenPos = eventCamera.WorldToScreenPoint(worldPos);
// 如果有Canvas,且Canvas的overrideSorting属性为true,则上层不再影响它们
bool continueTraversal = true;
bool valid = true;
List<Component> components = new List<Component>();
while (t != null)
{
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
{
continueTraversal = true;
}
var mask = components[i] as Mask;
var rectMask2D = components[i] as RectMask2D;
if (mask == null && rectMask2D == null)
{
continue;
}
var filter = components[i] as ICanvasRaycastFilter; // 很有意思一点,对于Mask和RectMask2D 点不到跟看不到一个道理
valid = filter.IsRaycastLocationValid(screenPos, eventCamera);
if (!valid)
{
return true;
}
}
t = continueTraversal ? t.parent : null;
}
return false;
}
/// <summary>
/// 判断Graphic是否在屏幕范围内,仅讨论canvas renderMode是Camera的情况
/// </summary>
/// <param name="graphic"></param>
/// <param name="eventCamera"></param>
/// <returns></returns>
private bool InScreen(Canvas canvas, PointerEventData eventData, Camera eventCamera)
{
// 该情况过于复杂,大概说下流程
// 1. 首先调用Display.RelativeMouseAt(eventData.position)方法来获取事件相对于屏幕的返回
// 2. 如果返回的位置不是零向量(Vector3.zero),则表示事件确实发生在某显示器上
// 3. 返回的事件位置(eventPosition)的z会作为显示器索引
// 4. 如果返回零向量,则意为多显示器系统在该平台上不受支持,在这种情况下,会假定认为是事件在当前显示器上发生
// 5. 最终根据显示器索引获得显示器分辨率(Display.displays[index])
// 6. 判断鼠标位置是否有超出屏幕分辨率
int displayIndex = canvas.targetDisplay;
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int) eventPosition.z;
if (eventDisplayIndex != displayIndex)
{
return false;
}
}
else
{
eventPosition = eventData.position;
}
Vector2 pos;
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || eventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
{
pos = eventCamera.ScreenToViewportPoint(eventPosition);
}
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
{
return false;
}
return true;
}
}
本文作者:陈侠云
本文链接:https://www.cnblogs.com/chenxiayun/p/18122660
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步