开头很简单,最难的是坚持。|

陈侠云

园龄:2年10个月粉丝:1关注:1

Unity怎么判断图形(Graphic)是否被遮挡

起因:

最近领了个需求,需要给项目的弱引导增加个功能,判断它是否被其他UI遮挡住,如果被遮挡了就需要实时将它隐藏,遮挡结束则恢复显示,这个需求乍一看似乎有点不太变态,但细细想想似乎还是能够做到,以下将我的经验分享一下。

遮挡判断:

要想解决这个问题,最重要的是要知道一个ui怎么算是被遮挡了。

  • 第一个思路是,我认为一张图像的组成是由贴图采样得到的像素点中所有RGBA值中所有alpha值大于0的部分, 我们需要用最后输出到屏幕上的贴图和原贴图做逐个像素的对比,如果有不一致的地方则认为被遮挡了。但这个明显实现起来过于耗,而且结果不一定尽人意,假如仅有一个像素点被覆盖了,我们也认为它被遮挡了是比较离谱的,虽然我们可以定义个百分比的阙值,但毕竟还是对需求妥协了,所以不如换个更简单的思路。

  • 第二个思路,判断网格中所有顶点的采样得到像素值是否被其他颜色覆盖了,这样确实简化了不少,但是要想判断像素值,势必要开启所有图集资源的读写,仅仅是为了这个需求显然是不合理。

  • 第三个思路,所以我们简化一步到位,直接判断ui的中心点,并且不判断它的像素值,而是通过一条射线找到该点上的所有graph组件,剔除掉透明的组件并排序好层级关系后,判断该ui是否被遮挡。

在遮挡这,我们即可采取第三个思路来做处理,还是额外说明一点,如果graph是目标子节点,也可以认为它并没遮挡目标,因为它本身就是目标节点的一部分。

剔除看不见的UI

我们找到了该点上的所有UI,自然要剔除掉那些看不见的UI,我大致总结下有哪些因素会导致指向目标我们“看不见”

  1. 目标的gameObject.activeInHierarchy为false

  2. 目标graph的alpha值为0

  3. 目标上有CanvasGroup组件或其父对象上有CanvasGroup组件,且CanvasGroup组件的alpha值为0

  4. 目标被Mask组件或者RectMask2D组件裁剪

  5. 目标的canvasRender组件的cull属性为false(cull:指示是否忽略该渲染器发射的几何形状)

  6. 目标的canvasRender组件的absoluteDepth属性小于0(absoluteDepth:渲染器相对于根画布的深度)

  7. 目标不在屏幕范围内

思路总结

综上所述,我们大致就能得出一个做法,取得“指向目标”的屏幕坐标,找到屏幕上所有包含该坐标的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 中国大陆许可协议进行许可。

posted @   陈侠云  阅读(454)  评论(0编辑  收藏  举报
//雪花飘落效果
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起