前言
本文仅当学习笔记留存。
业务情景与效果展示
玩家解锁了一个功能,此时进行一个点击按钮的引导,这个引导需要排查其它视觉影响因素,如将按钮的位置之外的地方进行压黑,同时让玩家仅可以点击交互按钮。
核心代码
矩形遮罩Shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
//矩形遮罩Shader
Shader "UI/Guidance/RectGuidance"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
_Center("Center",vector) = (0,0,0,0)
_SliderX("SliderX",Range(0,1500)) = 1500
_SliderY("SliderY",Range(0,1500)) = 1500
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float2 _Center;
float _SliderX;
float _SliderY;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = v.texcoord;
OUT.color = v.color * _Color;
return OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
float2 dis = IN.worldPosition.xy - _Center.xy;
color.a *= (abs(dis.x) > _SliderX) || (abs(dis.y) > _SliderY);
color.rgb *= color.a;
return color;
}
ENDCG
}
}
}
矩形遮罩核心脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 矩形引导组件
/// </summary>
public class RectGuidanceController : MonoBehaviour, ICanvasRaycastFilter {
//获取画布
public Canvas canvas;
/// <summary>
/// 高亮显示的目标
/// </summary>
public RectTransform target;
/// <summary>
/// 区域范围缓存
/// </summary>
private Vector3[] corners = new Vector3[4];
/// <summary>
/// 镂空区域中心
/// </summary>
private Vector4 center;
/// <summary>
/// 最终的偏移值X
/// </summary>
private float targetOffsetX = 0f;
/// <summary>
/// 最终的偏移值Y
/// </summary>
private float targetOffsetY = 0f;
/// <summary>
/// 遮罩材质
/// </summary>
private Material material;
/// <summary>
/// 当前的偏移值X
/// </summary>
private float currentOffsetX = 0f;
/// <summary>
/// 当前的偏移值Y
/// </summary>
private float currentOffsetY = 0f;
/// <summary>
/// 动画收缩时间
/// </summary>
public float shrinkTime = 0.5f;
private float shrinkVelocityX = 0f;
private float shrinkVelocityY = 0f;
/// <summary>
/// 世界坐标到画布坐标的转换
/// </summary>
/// <param name="canvas">画布</param>
/// <param name="world">世界坐标</param>
/// <returns>转换后在画布的坐标</returns>
private Vector2 WorldToCanvasPos(Canvas canvas, Vector3 world) {
Vector2 position;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, world,
canvas.GetComponent<Camera>(), out position);
return position;
}
public void SetTarget(RectTransform target) {
this.target = target;
RefreshMask();
}
public void RefreshMask() {
//获取高亮区域四个顶点的世界坐标
target.GetWorldCorners(corners);
//计算高亮显示区域咋画布中的范围
targetOffsetX = Vector2.Distance(WorldToCanvasPos(canvas, corners[0]), WorldToCanvasPos(canvas, corners[3])) / 2f;
targetOffsetY = Vector2.Distance(WorldToCanvasPos(canvas, corners[0]), WorldToCanvasPos(canvas, corners[1])) / 2f;
//计算高亮显示区域的中心
float x = corners[0].x + ((corners[3].x - corners[0].x) / 2f);
float y = corners[0].y + ((corners[1].y - corners[0].y) / 2f);
Vector3 centerWorld = new Vector3(x, y, 0);
Vector2 center = WorldToCanvasPos(canvas, centerWorld);
//设置遮罩材料中中心变量
Vector4 centerMat = new Vector4(center.x, center.y, 0, 0);
material = GetComponent<Image>().material;
material.SetVector("_Center", centerMat);
//计算当前偏移的初始值
RectTransform canvasRectTransform = (canvas.transform as RectTransform);
if (canvasRectTransform != null) {
//获取画布区域的四个顶点
canvasRectTransform.GetWorldCorners(corners);
//求偏移初始值
for (int i = 0; i < corners.Length; i++) {
if (i % 2 == 0)
currentOffsetX = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, corners[i]), center), currentOffsetX);
else
currentOffsetY = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, corners[i]), center), currentOffsetY);
}
}
//设置遮罩材质中当前偏移的变量
material.SetFloat("_SliderX", currentOffsetX);
material.SetFloat("_SliderY", currentOffsetY);
}
private void Update() {
//从当前偏移值到目标偏移值差值显示收缩动画
float valueX = Mathf.SmoothDamp(currentOffsetX, targetOffsetX, ref shrinkVelocityX, shrinkTime);
float valueY = Mathf.SmoothDamp(currentOffsetY, targetOffsetY, ref shrinkVelocityY, shrinkTime);
if (!Mathf.Approximately(valueX, currentOffsetX)) {
currentOffsetX = valueX;
material.SetFloat("_SliderX", currentOffsetX);
}
if (!Mathf.Approximately(valueY, currentOffsetY)) {
currentOffsetY = valueY;
material.SetFloat("_SliderY", currentOffsetY);
}
}
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) {
if (target == null) return false;
bool InRect = RectTransformUtility.RectangleContainsScreenPoint(target.GetComponent<RectTransform>(), sp, eventCamera);
if (InRect)
Debug.Log("点击可以穿透并能点中按钮");
else
Debug.LogWarning("点不中按钮的,死心吧");
return !InRect;
}
}
重点方法:
这个功能中需要能点击没有被遮罩盖住的按钮,但被遮罩的地方不能被点击
先介绍两个unity的方法
IsRaycastLocationValid
需要接口ICanvasRaycastFilter
它的作用是:给定一个点和一个摄像机,判断射线投射是否有效。返回 false 则这个物体将不会被射线击中。
RectTransform.RectangleContainsScreenPoint
是 Unity 中 RectTransform
类的一个方法,用于判断一个屏幕空间下的点是否在该 RectTransform 所代表的矩形内。
将两个方法结合起来就是
//点击穿透
public
bool
IsRaycastLocationValid(Vector2 sp, Camera eventCamera) {
if
(target ==
null
)
return
false
;
bool InRect = RectTransformUtility.RectangleContainsScreenPoint(target.GetComponent<RectTransform>(), sp, eventCamera);
if (InRect)
Debug.Log("点击可以穿透并能点中按钮");
else
Debug.LogWarning("点不中按钮的,死心吧");
//取反是因为
IsRaycastLocationValid针对的是当前挂载的对象,也就是ImgMask,返回 false 则ImgMask将不会被射线击中
return !InRect;
}
}
注意
RectTransformUtility.RectangleContainsScreenPoint使用时
当场景中的Canvas为Screen Space -Overlay时:
RectTransformUtility.RectangleContainsScreenPoint(target.GetComponent<RectTransform>(), sp);
这样写是正常的
但当你场景中的Canvas为Screen Space -Camera时:
仅传两个参数时始终都返回 False,需要和我那样使用三个参数才可以正常使用
圆形遮罩Shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "UI/Guidance/CircleGuidance"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
_Center("Center",vector) = (0,0,0,0)
_Slider("Slider",Range(0,1500)) = 1500
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float2 _Center;
float _Slider;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = v.texcoord;
OUT.color = v.color * _Color;
return OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
color.a *= (distance(IN.worldPosition.xy, _Center.xy) > _Slider);
color.rgb *= color.a;
return color;
}
ENDCG
}
}
}
圆形遮罩核心脚本
/// <summary>
/// 圆形遮罩镂空引导
/// </summary>
public class CircleGuidanceController : MonoBehaviour, ICanvasRaycastFilter {
public Canvas canvas;
/// <summary>
/// 镂空区域最终半径
/// </summary>
public float radius = 200f;
/// <summary>
/// 镂空区域初始半径
/// </summary>
public float initialRadius = 600f;
/// <summary>
/// 当前镂空区域的半径
/// </summary>
private float currentRadius = 600f;
/// <summary>
/// 镂空区域圆心
/// </summary>
Vector2 center;
/// <summary>
/// 镂空区域圆心屏幕坐标
/// </summary>
Vector2 targetScreenPos;
/// <summary>
/// 遮罩材质
/// </summary>
private Material material;
/// <summary>
/// 高亮区域缩放的动画时间
/// </summary>
public float shrinkTime = 0.5f;
/// <summary>
/// 收缩速度
/// </summary>
float shrinkVelocity = 0f;
/// <summary>
/// 世界坐标向画布坐标转换
/// </summary>
/// <param name="canvas">画布</param>
/// <param name="world">世界坐标</param>
/// <returns>返回画布上的二维坐标</returns>
private Vector2 ScreenToCanvasPos(Canvas canvas, Vector3 world)
{
Vector2 position;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent<RectTransform>(),
world, UIManager.Instance.UICamera, out position);
return position;
}
public void RefreshMask(Vector3 targetScreenPos)
{
this.targetScreenPos = targetScreenPos;
center = ScreenToCanvasPos(canvas, this.targetScreenPos);
//设置遮罩材料中的圆心变量
Vector4 centerMat = new Vector4(center.x, center.y, 0, 0);
material = GetComponent<Image>().material;
currentRadius = initialRadius;
material.SetVector("_Center", centerMat);
material.SetFloat("_Slider", currentRadius);
}
private void Update()
{
//从当前半径到目标半径差值显示收缩动画
float value = Mathf.SmoothDamp(currentRadius, radius, ref shrinkVelocity, shrinkTime);
if (!Mathf.Approximately(value, currentRadius))
{
currentRadius = value;
material.SetFloat("_Slider", currentRadius);
}
}
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) {
return InCircle(sp);
}
bool InCircle(Vector2 sp) {
float distanceToCenter = Vector2.Distance(this.targetScreenPos, sp);
// 判断距离是否小于等于半径
if (distanceToCenter <= radius) {
Debug.Log("点击位置在圆内");
} else {
Debug.LogWarning("点击位置不在圆内");
}
return distanceToCenter <= radius;
}
}
圆形遮罩需要调用RefreshMask(Vector3 targetScreenPos)方法来启用,传入的参数是屏幕坐标位置,即圆心位置。
使用方式
1.创建一个全撑的Image
2.设置Image的材质
3.挂载脚本并绑定需要的节点
当然,上面的仅仅是基础的效果展示,具体的使用需要根据业务场景进行调整!!!
大佬的链接