博客文章

Unity做任意图像的镂空

作者: Andy.      时间: 2022-01-05 21:39:11

任意图像的镂空应用最多的地方应该是新手引导。比如要引导玩家去点一个按钮,那么我们用一个遮罩将除了这个按钮的其他区域全部弄成黑色半透明,只将需要点击的按钮或者强调的按钮给凸显出来。这个效果还是非常棒的。为了能够完美达成这种情况,直接生成一张贴图,贴图是半透明的,但是对应按钮轮廓的位置是完全透明的。实现这种效果有两个方式:

1、 用Stencil生成。

2、 直接改混合函数生成。

我喜欢用第二种方法,因为简单。在新手引导的时候再通过ICanvasRaycastFilter进行过滤,让用户只能点突出那个位置就达成了目标。

最后达到的效果如下:

image.png

我们将遮盖层拿远一点儿:

image.png

 

之前有一篇文章讲Cocos Creator如何实现一套成熟的新手引导中讲过第二种方法。但是博客数据丢失了,(;´д`)ゞ………………,在Unity中实现的要比那个麻烦一些。

举例:Cocos中实现,只需要:

1、 创建一个半透明的RenderTexture,然后设置上面的Back按钮和子弹按钮渲染的时候,渲染函数为GL_ZERO和GL_ONE_MINUS_SRC_ALPHA(具体原理看:LearnOpenGL - Blending)。可以通过代码设置。

2、 然后只渲染这两个节点就可以了。

于是按照这个思路在Unity里面实现,发现都出现了问题。

先说第一步,按照这个思路,广泛搜索文档后发现不行,这个在Unity中只能通过Shader设置。于是,找到对应版本的Shader,下载下来后,找到默认的UI Shader:

image.png

拷贝一份,创建一个新的Shader,取名UIHollowOut.shader,粘贴进去,将“Blend One OneMinusSrcAlpha”修改为“Blend Zero OneMinusSrcAlpha”,创建对应的材质球:UIHollowOut.mat。

这样我们直接Instantiate一份要镂空的节点,将material改成UIHollowOut,然后渲染到RenderTexture里面就OK。

于是我就到了第二步,只渲染镂空的节点,但是事实上好像和我想象的有很大差别。本来想的是通过layer来区分,但是发现Canvas下面的节点好像不生效。经过验证发现:建立一个layer叫Guidance,然后设置摄像机只渲染Guidance这个layer,设置Canvas下某个节点layer为Guidance,渲染不到。但是设置单单Canvas为Guidance,整个UI都被渲染了。

于是我选择了一个笨办法:

1、 创建另外一个Canvas和Camera。设置Camera,clear为一个半透明的Color。

2、 创建一个临时的RenderTexture。将Camera输出到这个RenderTexture。

3、 将Instantiate出来的Object放在这个Canvas下面。并设置material为镂空的material。

4、 渲染得到想要的镂空Texture。

5、 创建一个RawImage放在UI的Canvas上面。就得到了这种不规则物体的镂空效果。

6、 为了让玩家能点且仅能点想要的Button,如果需要给RawImage添加一个脚本,实现ICanvasRaycastFilter接口。过滤一下点击。

 

最后节点和代码如下:

image.png

引导(镂空)的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class GuidanceManager : Singleton<GuidanceManager>
{
    public Transform GuidanceHelperNode { get; set; } = null;
    public Camera GuidanceHelperCamera { get; set; } = null;
    public Canvas GuidanceHelperCanvas { get; set; } = null;
    public Canvas UICanvas { get; set; } = null;
    public void StartGuidance(UnityEngine.UI.Image[] objs, RectTransform clickTransform)
    {
        if (GuidanceHelperNode == null) return;
        if (GuidanceHelperCamera == null) return;
        if (GuidanceHelperCanvas == null) return;
        if (UICanvas == null) return;

        // enable helper node.
        GuidanceHelperNode.gameObject.SetActive(true);

        // create mask texture.
        var canvasRectTransform = GuidanceHelperCanvas.GetComponent<RectTransform>();
        RenderTexture renderTexture = new RenderTexture((int)Math.Ceiling(canvasRectTransform.sizeDelta.x), (int)Math.Ceiling(canvasRectTransform.sizeDelta.y), 16);
        GuidanceHelperCamera.targetTexture = renderTexture;
        GuidanceHelperCamera.clearFlags = CameraClearFlags.SolidColor;
        GuidanceHelperCamera.backgroundColor = new Color(0, 0, 0, (float)0.4);

        // load hollow out mat.
        var mat = ResourceManager.Instance.LoadResource<Material>("Assets/Res/Shader/UIHollowOut.mat");

        for (int i = canvasRectTransform.childCount - 1; i >= 0; i--)
        {
            GameObject.Destroy(canvasRectTransform.GetChild(i));
        }
        // instantiate object which need to hollow out.
        for (int i = 0; i < objs.Length; i++)
        {
            if (!(objs[i].gameObject is GameObject)) continue;
            var obj = objs[i].gameObject as GameObject;
            var objRectTransForm = obj.GetComponent<RectTransform>();
            var tempTransform = UnityEngine.Object.Instantiate(obj, canvasRectTransform).GetComponent<RectTransform>();
            tempTransform.position = objRectTransForm.position;
            tempTransform.sizeDelta = objRectTransForm.sizeDelta;
            tempTransform.GetComponent<Image>().material = mat;
        }
        GuidanceHelperCamera.Render();
        ResourceManager.Instance.ReleaseResource(mat);

        // create game object which display mask texture.
        GameObject gameObject = new GameObject();
        gameObject.GetComponent<Transform>().SetParent(UICanvas.GetComponent<RectTransform>());
        var rawImage = gameObject.AddComponent<RawImage>();
        gameObject.AddComponent<MaskTexture>().ClickRectTransform = clickTransform;
        rawImage.texture = renderTexture;
        var maskRectTransform = gameObject.GetComponent<RectTransform>();
        maskRectTransform.localScale = new Vector3(1, 1, 1);
        maskRectTransform.localPosition = new Vector2(0, 0);
        maskRectTransform.sizeDelta = new Vector2(Screen.width, Screen.height);

        GuidanceHelperNode.gameObject.SetActive(false);
    }

}

测试的场景代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TestScene : MonoBehaviour
{
    public Transform GuidanceHelperNode;
    public Camera GuidanceHelperCamera;
    public Canvas GuidanceHelperCanvas;

    public Button BtnBack;
    public Button ImgBullet;

    public void OnClickStartGuidance()
    {
        GuidanceManager.Instance.GuidanceHelperNode = GuidanceHelperNode;
        GuidanceManager.Instance.GuidanceHelperCamera = GuidanceHelperCamera;
        GuidanceManager.Instance.GuidanceHelperCanvas = GuidanceHelperCanvas;
        GuidanceManager.Instance.UICanvas = GetComponentInParent<Canvas>();

        GuidanceManager.Instance.StartGuidance(new Image[] { BtnBack.image, ImgBullet.image }, BtnBack.GetComponent<RectTransform>());
    }

    public void OnClickBack()
    {
        Debug.Log("OnClick Back.");
    }

    public void OnClickBullet()
    {
        Debug.Log("OnClick Bullet.");
    }

}

点击过滤的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MaskTexture : MonoBehaviour, ICanvasRaycastFilter
{
    public RectTransform ClickRectTransform { get; set; } = null;

    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        if (ClickRectTransform == null) return true;

        return !RectTransformUtility.RectangleContainsScreenPoint(ClickRectTransform, sp, eventCamera);
    }
}