转载自 https://www.taidous.com/forum.php?mod=viewthread&fid=211&tid=55259

我想大家在用uGUI做界面时,可能经常会碰到一种需求,就是在图片上“挖洞”。

说起来我们可以有几种实现方案,比如最简单的方式,直接导入带有“洞”的图片。这种方式简单,但不适合需要动态变化的场合。考虑有这种需求:当我们上线一个新功能时,可能希望在玩家第一次打开游戏时,将界面其它地方变暗,突出新增的功能,即所谓的“新手引导”功能。

如果用黑色含透明区域图片来展示这种效果,也不是不可以,但是会有几个问题。首先需要处理UI遮挡问题,因为Image的透明区域依然会阻挡下层的点击事件;其次如果新手引导分若干步,每步要展示的区域大小和形状都不同,那可能需要针对每步都做图,这个过程会变得非常复杂。

反过来考虑,如果我们可以实现将图片上任意形状区域“剔除”的功能,不就刚好可以满足这种需求吗?这就是本文所讨论的重点:图片“挖洞”的一种实现手段。

首先明确下我们的需求:

    • 我们需要能将图片中某个形状区域隐藏显示;
    • 最好能够让点击事件穿过此区域;
          • 此时熟悉uGUI的同学可能已经发现了,uGUI内置了一种组件叫做Mask,恰好实现了这两种需求(的大部分)。我们先来分析下Mask。

            Mask的设计思路是这样的:它与Image组件配合工作,根据Image的覆盖区域来定位显示范围,所有此Image的子级UI元素,超出此区域的部分都会被隐藏(包括UI交互事件)。
            于是我们发现,我们想要实现的功能与Mask组件似乎恰好相反:我们是想要此Image覆盖区域的子级UI元素不显示,而超出区域的部分照常显示,这样即可(初步)满足需求。

            那么我们看下Mask的实现原理吧,看看是不是可以借鉴思路呢?Unity官方文档关于Mask的实现原理说明如下:

          • 可以简单理解为:Mask会将Image的渲染区域像素进行特别标记,稍后子级UI进行像素渲染时,判断如果存在此标记(说明渲染像素位于Mask区域内)就进行渲染,否则不渲染。可以发现,此功能的实现除了Mask组件,还需要子级UI元素的配合。实际上,Unity的内置UI组件都继承自MaskableGraphic,此类型正是Mask的配合实现者,它的相关代码实现如下:
            • public virtual Material GetModifiedMaterial(Material baseMaterial) { var toUse = baseMaterial; if (m_ShouldRecalculateStencil) { var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0; m_ShouldRecalculateStencil = false; } // if we have a enabled Mask component then it will // generate the mask material. This is an optimisation // it adds some coupling between components though :( Mask maskComponent = GetComponent<Mask>(); if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive())) { var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMat; toUse = m_MaskMaterial; } return toUse; }

              • 知道了Mask的原理,那么我们就会想到一种可能的方案,如果重写MaskableGraphic的GetModifiedMaterial方法,将它的判断逻辑逆转,是否就可以了呢?来试一下吧!新建脚本HoleImage,内容如下:
                1. public class HoleImage : Image {
                2. public override Material GetModifiedMaterial(Material baseMaterial)
                3. {
                4. var toUse = baseMaterial;
                5.  
                6. if (m_ShouldRecalculateStencil)
                7. {
                8. var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
                9. m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
                10. m_ShouldRecalculateStencil = false;
                11. }
                12.  
                13. // if we have a enabled Mask component then it will
                14. // generate the mask material. This is an optimisation
                15. // it adds some coupling between components though :(
                16. Mask maskComponent = GetComponent<Mask>();
                17. if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                18. {
                19. var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.NotEqual, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                20. StencilMaterial.Remove(m_MaskMaterial);
                21. m_MaskMaterial = maskMat;
                22. toUse = m_MaskMaterial;
                23. }
                24. return toUse;
                25. }
                26. }
              • 本帖最后由 younglee 于 2017-3-22 21:33 编辑

                我想大家在用uGUI做界面时,可能经常会碰到一种需求,就是在图片上“挖洞”。

                说起来我们可以有几种实现方案,比如最简单的方式,直接导入带有“洞”的图片。这种方式简单,但不适合需要动态变化的场合。考虑有这种需求:当我们上线一个新功能时,可能希望在玩家第一次打开游戏时,将界面其它地方变暗,突出新增的功能,即所谓的“新手引导”功能。

                如果用黑色含透明区域图片来展示这种效果,也不是不可以,但是会有几个问题。首先需要处理UI遮挡问题,因为Image的透明区域依然会阻挡下层的点击事件;其次如果新手引导分若干步,每步要展示的区域大小和形状都不同,那可能需要针对每步都做图,这个过程会变得非常复杂。

                反过来考虑,如果我们可以实现将图片上任意形状区域“剔除”的功能,不就刚好可以满足这种需求吗?这就是本文所讨论的重点:图片“挖洞”的一种实现手段。

                首先明确下我们的需求:

                • 我们需要能将图片中某个形状区域隐藏显示;
                • 最好能够让点击事件穿过此区域;

                此时熟悉uGUI的同学可能已经发现了,uGUI内置了一种组件叫做Mask,恰好实现了这两种需求(的大部分)。我们先来分析下Mask。

                Mask的设计思路是这样的:它与Image组件配合工作,根据Image的覆盖区域来定位显示范围,所有此Image的子级UI元素,超出此区域的部分都会被隐藏(包括UI交互事件)。
                于是我们发现,我们想要实现的功能与Mask组件似乎恰好相反:我们是想要此Image覆盖区域的子级UI元素不显示,而超出区域的部分照常显示,这样即可(初步)满足需求。

                那么我们看下Mask的实现原理吧,看看是不是可以借鉴思路呢?Unity官方文档关于Mask的实现原理说明如下:

                Implementation
                  Masking is implemented using the stencil buffer of the GPU.
                  The first Mask element writes a 1 to the stencil buffer All elements below the mask check when rendering, and only render to areas where there is a 1 in the stencil buffer *Nested Masks will write incremental bit masks into the buffer, this means that renderable children need to have the logical & of the stencil values to be rendered.

                可以简单理解为:Mask会将Image的渲染区域像素进行特别标记,稍后子级UI进行像素渲染时,判断如果存在此标记(说明渲染像素位于Mask区域内)就进行渲染,否则不渲染。可以发现,此功能的实现除了Mask组件,还需要子级UI元素的配合。实际上,Unity的内置UI组件都继承自MaskableGraphic,此类型正是Mask的配合实现者,它的相关代码实现如下:

                [AppleScript] 纯文本查看 复制代码
                1. public virtual Material GetModifiedMaterial(Material baseMaterial)
                2. {
                3. var toUse = baseMaterial;
                4.  
                5. if (m_ShouldRecalculateStencil)
                6. {
                7. var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
                8. m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
                9. m_ShouldRecalculateStencil = false;
                10. }
                11.  
                12. // if we have a enabled Mask component then it will
                13. // generate the mask material. This is an optimisation
                14. // it adds some coupling between components though :(
                15. Mask maskComponent = GetComponent<Mask>();
                16. if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                17. {
                18. var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                19. StencilMaterial.Remove(m_MaskMaterial);
                20. m_MaskMaterial = maskMat;
                21. toUse = m_MaskMaterial;
                22. }
                23. return toUse;
                24. }

                知道了Mask的原理,那么我们就会想到一种可能的方案,如果重写MaskableGraphic的GetModifiedMaterial方法,将它的判断逻辑逆转,是否就可以了呢?来试一下吧!新建脚本HoleImage,内容如下:

                [AppleScript] 纯文本查看 复制代码
                1. public class HoleImage : Image {
                2. public override Material GetModifiedMaterial(Material baseMaterial)
                3. {
                4. var toUse = baseMaterial;
                5.  
                6. if (m_ShouldRecalculateStencil)
                7. {
                8. var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
                9. m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
                10. m_ShouldRecalculateStencil = false;
                11. }
                12.  
                13. // if we have a enabled Mask component then it will
                14. // generate the mask material. This is an optimisation
                15. // it adds some coupling between components though :(
                16. Mask maskComponent = GetComponent<Mask>();
                17. if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                18. {
                19. var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.NotEqual, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                20. StencilMaterial.Remove(m_MaskMaterial);
                21. m_MaskMaterial = maskMat;
                22. toUse = m_MaskMaterial;
                23. }
                24. return toUse;
                25. }
                26. }

                注意,我们唯一改动的地方在19行,将CompareFunction.Equal改为了CompareFunction.NotEqual,即只有没有被Mask标记的区域才进行渲染。回到Unity,在Canvas下新建一个较小的Image,添加Mask组件,取消勾选“Show Mask Graphic”,并添加一个较大的子级Image,可以发现子级Image已经正确地被Mask组件给挖出了一个洞。

                至此,本文的核心问题已经被解决,这个简陋的东西已经可以解决一些问题。接下来我们对它进行进一步处理完善。第一个小问题很容易就暴露了,你会发现游戏运行中当你点击图片空洞时,UI事件并不会传递到下层,反而点击Mask外部区域却传递了UI事件,实际上这正是Mask期望的结果,但却不是我们期望的结果~

                这个问题不难解决,我们来分析下uGUI的UI事件传递机制。uGUI通过ICanvasRaycastFilter接口来处理UI捕获,相关方法如下:

  1. bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
  2.  
  3. UI对象需要实现此接口来自定义焦点捕获判断逻辑。当某个区域坐标被点击时,系统会对当前区域所有UI元素(从顶层到底层)调用此方法,第一个返回true的元素即认定为捕获点击。稍微复杂一点的是嵌套的UI结构。对于某个UI元素,如果它是被嵌套的,那么这个接口调用会从它自身开始,逐级向上调用它的父级UI元素,此过程中任意层级返回false系统都会立刻终止判断,认为此UI不能捕获点击。简单理解的话,就是某个UI元素想要响应某个点击,除了看它自身的意愿,还要看 历史的进程 它爹的意愿,而它爹同意后还要看它爷爷的意见……
  4.  
  5. 所以我们直接从源头做起,干掉它爹——也就是Mask的判断逻辑即可。Mask原本是这样处理的:
[AppleScript] 纯文本查看 复制代码
  1. public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
  2. {
  3. if (!isActiveAndEnabled)
  4. return true;
  5.  
  6. return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
  7. }
  1.  
  2. 嗯,跟我们预期的一样简单粗暴:不在我自身“势力范围”内的统统返回false。那么同样的,我们只需要反转此逻辑即可。新建脚本Hole,内容如下:
[AppleScript] 纯文本查看 复制代码
  1. public class Hole : Mask
  2. {
  3. public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
  4. {
  5. if (!isActiveAndEnabled)
  6. return true;
  7.  
  8. return !RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
  9. }
  10. }
  1.  
  2. 将场景中的Mask替换为Hole,运行测试,会发现UI事件已经按照新的逻辑执行了。
  3.  
  4. 至此,通过Hole替代MaskHoleImage替代Image,我们开头提到的需求已经能够完整解决了。其实还有一个小问题,我们的Hole完整的继承了Mask的逻辑,只是反转了UI事件检测,这也就意味着……对,它的其它子级UI元素依然会表现出Mask的作用。这在一些情形下可能并不是你想要的结果。那么,你能想到用什么方式来解决此问题吗?

UnityGUI扩展实例:图片挖洞效果 Mask的反向实现的更多相关文章

  1. 实例源码--Android图片滚动切换效果

    下载源码 技术要点:  1.图片滚动切换技术 2.详细的源码注释 ...... 详细介绍: 1.图片滚动切换技术 本套源码实现了类似于网站图片滚动推广效果,效果不错,很不错的参考源码 2.源码目录 运 ...

  2. 纯CSS3写的10个不同的酷炫图片遮罩层效果【转】

    这个是纯CSS3实现的的10个不同的酷炫图片遮罩层效果,可以欣赏一下 在线预览 下载地址 实例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...

  3. 纯CSS3写的10个不同的酷炫图片遮罩层效果

    这个是纯CSS3实现的的10个不同的酷炫图片遮罩层效果,可以欣赏一下 在线预览 下载地址 实例代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1 ...

  4. jQuery演示10种不同的切换图片列表动画效果以及tab动画演示 2

    很常用的一款特效纯CSS完成tab实现5种不同切换对应内容效果 实例预览 下载地址 实例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ...

  5. 利用Clip制作打洞效果

    起因 如上篇博文所说,连线原型需要在中间文字上下各留15像素的空白.设计师完成原型之后,问我有没有办法实现,我说,我能想到两种实现方式.其中一种就是上篇博文所说的OpacityMask.第二种就是使用 ...

  6. 【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 18—Photo OCR 应用实例:图片文字识别

    Lecture 18—Photo OCR 应用实例:图片文字识别 18.1 问题描述和流程图 Problem Description and Pipeline 图像文字识别需要如下步骤: 1.文字侦测 ...

  7. [JQuery]用InsertAfter实现图片走马灯展示效果2——js代码重构

    写在前面 前面写过一篇文章<[JQuery]用InsertAfter实现图片走马灯展示效果>,自从写过那样的也算是使用面向对象的写法吧,代码实在丑陋,自从写过那样的代码,就是自己的一块心病 ...

  8. jQuery演示10种不同的切换图片列表动画效果

    经常用到的图片插件演示jQuery十种不同的切换图片列表动画效果 在线演示 下载地址 实例代码 <!DOCTYPE html> <html lang="en" c ...

  9. unity, 挖洞特效

    想模仿这个游戏的挖洞特效: 思路: 效果: 代码下载:http://pan.baidu.com/s/1kUN8goZ

随机推荐

  1. 20170330 webservice代理类测试

    代理类测试 执行事物码SE80,找到之前创建好的代理类,如下图所示: 双击该代理类,进入其显示界面,如下图所示: 点击执行按钮,或者快捷键F8.如下图所示:. 逻辑端口文本框就是之前创建的逻辑端口技术 ...

  2. MCU与FPGA通信

    1.MCU启动FPGA相应功能模块 通过译码器选择相应的功能模块,调用实现功能. 2.MCU与FPGA串口通信 SPI协议简单.可靠.易实现,速度快,推荐使用SPI.SPI为四线机制,包含MOSI.M ...

  3. C语言运算符优先级误解

    优先级问题 表达式 可能误以为的结果 实际结果 .的优先级高于*. ->操作符用于消除这个问题 *p.f p所指对象的字段f. (*p).f 对p去f偏移,作为指针,然后进行解除引用操作. *( ...

  4. Python基础(12)_python模块之sys模块、logging模块、序列化json模块、pickle模块、shelve模块

    5.sys模块 sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) sys.version 获取Python解释程序的版本信息 ...

  5. Loadrunder之脚本篇——参数化取值策略

    参数取值选项 Select next row Update value on 以上两个选项是改变参数化取值的关键选项. Select next row包含如下选项: Sequential:顺序选择 R ...

  6. point-to-point(点对点) 网口

    点对点连接是两个系统或进程之间的专用通信链路.想象一下直接连接两个系统的一条线路.两个系统独占此线路进行通信.点对点通信的对立面是广播,在广播通信中,一个系统可以向多个系统传输. 点对点通信在OSI协 ...

  7. Linux基础三---打包压缩&vim&系统的初始化和服务

    一,常用命令——tar&vim 1. tar [参数]  文件名  [路径] 参数: -c :建立一个压缩文件的参数指令(create 的意思):     -x :解开一个压缩文件的参数指令! ...

  8. 主攻ASP.NET.4.5.1 MVC5.0之重生:系统角色与权限(一)

    数据结构 权限分配 1.在项目中新建文件夹Helpers 2.在HR.Helpers文件夹下添加EnumMoudle.Cs namespace HR.Helpers { public enum Enu ...

  9. Python 字符串概念和操作

    # 字符串概念:由单个字符串组成的一个集合 # 普通字符串(非原始字符串) str = "abc" print(str) # abc # 原始字符串(前面加r) str = r&q ...

  10. JAVA Excel导入导出

    --------------------------------------------方式一(新)-------------------------------------------------- ...