介绍
这里要介绍的是通过着色器(Shader)来实现高亮某一UI
效果
先看效果
一个圆形范围高亮显示,一个矩形范围显示
圆形
这里先上C#代码,后面会给出Shader的代码
/// <summary>
/// 圆形遮罩镂空引导
/// </summary>
public class CircleGuidanceController : MonoBehaviour
{
/// <summary>
/// 要高亮显示的目标
/// </summary>
public RectTransform Target;
/// <summary>
/// 区域范围缓存
/// </summary>
private Vector3[] _corners = new Vector3[4];
/// <summary>
/// 镂空区域圆心
/// </summary>
private Vector4 _center;
/// <summary>
/// 镂空区域半径
/// </summary>
private float _radius;
/// <summary>
/// 遮罩材质
/// </summary>
private Material _material;
/// <summary>
/// 当前高亮区域的半径
/// </summary>
private float _currentRadius;
/// <summary>
/// 高亮区域缩放的动画时间
/// </summary>
private float _shrinkTime = 0.5f;
/// <summary>
/// 时间渗透组件
/// </summary>
private GuidanceEventPenetrate _eventPenetrate;
/// <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,bool isPauseGame, float timer=0.5f)
{
this.isPauseGame = isPauseGame;
Target = target;
_shrinkTime = timer;
_eventPenetrate = GetComponent<GuidanceEventPenetrate>();
if (_eventPenetrate != null)
_eventPenetrate.SetTarget(Target);
//获取画布
Canvas canvas = GameObject.Find("Canvas").GetComponent<Canvas>();
//获取高亮区域的四个顶点的世界坐标
Target.GetWorldCorners(_corners);
//计算最终高亮显示区域的半径
_radius = Vector2.Distance(WorldToCanvasPos(canvas, _corners[0]), WorldToCanvasPos(canvas, _corners[2])) / 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 canRectTransform = canvas.transform as RectTransform;
if (canRectTransform != null)
{
//获取画布区域的四个顶点
canRectTransform.GetWorldCorners(_corners);
//将画布顶点距离高亮区域中心最远的距离作为当前高亮区域半径的初始值
foreach (Vector3 corner in _corners)
{
_currentRadius = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, corner), center), _currentRadius);
}
}
_material.SetFloat("_Slider", _currentRadius);
if (mShink != null)
{
StopCoroutine(mShink);
}
if (isPauseGame)
{
mShink=StartCoroutine(StartShink());
}
}
private Coroutine mShink;
private bool isPauseGame;
/// <summary>
/// 脱离timeScale影响
/// </summary>
/// <returns></returns>
IEnumerator StartShink()
{
float length = _currentRadius - _radius;
for (float i = 0; i < _shrinkTime; i+= Time.unscaledDeltaTime)
{
float scale = i / _shrinkTime;
if (scale >= 1) scale = 1;
float value = _currentRadius - (length * scale);
_material.SetFloat("_Slider", value);
yield return 0;
}
_material.SetFloat("_Slider", _radius);
}
/// <summary>
/// 收缩速度
/// </summary>
private float _shrinkVelocity = 0f;
private void Update()
{
if (isPauseGame) return;
if (Target == null) return;
//从当前半径到目标半径差值显示收缩动画
float value = Mathf.SmoothDamp(_currentRadius, _radius, ref _shrinkVelocity, _shrinkTime);
if (!Mathf.Approximately(value, _currentRadius))
{
_currentRadius = value;
_material.SetFloat("_Slider", _currentRadius);
}
}
public void ClearTaget()
{
Target = null;
}
}
这里由于我项目中有的引导会把游戏暂停(timeScale),这样会导致收缩动画也被暂停,所以我增加了一个协程通道,通过协程来播放收缩动画。下面是圆形着色器代码:
Shader "UI/BeginnerGuidance/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
}
}
}
矩形
代码基本都有注释,挂载在UI上即可,UI材质球着色器使用对应的Shader
/// <summary>
/// 矩形引导组件
/// </summary>
public class RectGuidanceController : MonoBehaviour
{
/// <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>
private float _shrinkTime = 0.5f;
/// <summary>
/// 时间渗透组件
/// </summary>
private GuidanceEventPenetrate _eventPenetrate;
/// <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 rectTransform, bool isPauseGame, float timer=0.5f)
{
this.isPauseGame = isPauseGame;
Target = rectTransform;
_shrinkTime = timer;
_eventPenetrate = GetComponent<GuidanceEventPenetrate>();
if (_eventPenetrate != null)
_eventPenetrate.SetTarget(Target);
//获取画布
Canvas canvas = GameObject.Find("Canvas").GetComponent<Canvas>();
//获取高亮区域四个顶点的世界坐标
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);
if (mShink != null)
{
StopCoroutine(mShink);
}
if (isPauseGame)
{
mShink=StartCoroutine(StartShink());
}
}
private float _shrinkVelocityX = 0f;
private float _shrinkVelocityY = 0f;
private Coroutine mShink;
private bool isPauseGame; //是否受时间影响 true 影响 false 不影响
/// <summary>
/// 脱离timeScale影响
/// </summary>
/// <returns></returns>
IEnumerator StartShink()
{
float lengthX = _currentOffsetX - _targetOffsetX;
float lengthY = _currentOffsetY - _targetOffsetY;
for (float i = 0; i < _shrinkTime; i += Time.unscaledDeltaTime)
{
float scale = i / _shrinkTime;
if (scale >= 1) scale = 1;
float valueX = _currentOffsetX - (lengthX * scale);
_material.SetFloat("_SliderX", valueX);
float valueY= _currentOffsetY - (lengthY * scale);
_material.SetFloat("_SliderY", valueY);
yield return 0;
}
_material.SetFloat("_SliderX", _targetOffsetX);
_material.SetFloat("_SliderY", _targetOffsetY);
}
private void Update()
{
if (isPauseGame) return;
//从当前偏移值到目标偏移值差值显示收缩动画
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 void ClearTaget()
{
Target = null;
}
}
矩形着色器代码:
Shader "UI/BeginnerGuidance/Rect"
{
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
}
}
}
UI事件向下穿透
上面的内容只是实现了一个高亮缓动的动画,但是Raycast是被屏蔽了的,这个时候我们需要高亮的地方实现UI事件的穿透,让事件能向下传递。我们只需要实现Unity的ICanvasRaycastFilter接口
public class GuidanceEventPenetrate : MonoBehaviour, ICanvasRaycastFilter
{
private RectTransform mTagrget;
public void SetTarget(RectTransform target)
{
mTagrget = target;
}
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (mTagrget == null)
return true;
return !RectTransformUtility.RectangleContainsScreenPoint(mTagrget, sp, eventCamera);
}
}
RectangleContainsScreenPoint判断一个屏幕点是否在目标Rect范围内,如果在,则返回真
IsRaycastLocationValid为true时,事件向下传递是无效的,被拦截在当前UI界面,为false,则在当前界面是无效的
原理是当点击的位置在高亮目标的范围内,我们让当前的UI遮罩能向下传递事件,否则屏蔽事件