UI做刮刮卡的效果又是一搜一堆,代码都是复制粘贴那种,只能自己捣鼓一下。使用RenderTexure的是一种,不使用的比较少,但是原理没怎么说。这次使用的原理就是:开始的时候,取得遮罩图的像素数据,在鼠标滑动的时候,将屏幕坐标转换到遮罩在父节点下的坐标,然后以该点作为中心,生成需要擦除的区域大小,然后通过与刚开始记录的遮罩数据进行比较,替换需要擦除的区域的像素值,同时计算每一点像素的alpha值,在shader中做反转,达到透明的效果。绘制函数放在了鼠标拖动时进行,没有放在update中,现在唯一的不足就是拖动太快时没有做平滑处理,导致出现断块。但是没啥时间,先记录一下,没有像其他说的生成大量预设,透明区域的形状可以自己找图设定。下面上代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class MaskErase : MonoBehaviour, IDragHandler, IPointerDownHandler
{
// 取图片的父节点,转成本地坐标
public GameObject parent;
// 相机转行坐标使用
public Camera UICam;
// public int rectScale = 10;
// 控制形状
public Texture2D shapeTex;
Texture2D tempRt;
// 转成本地节点下的坐标下使用
RectTransform rectTrans;
private int imgWidth = 0;
private int imgHeight = 0;
private Material material;
// 数组保留像素值, 记录采样图的数据
float[,] samplePixel;
// 记录遮罩图的数据,判断是否可以替换当前像素的值
float[,] screenPixel;
void Start()
{
rectTrans = parent.GetComponent<RectTransform>();
Image localImg = gameObject.GetComponent<Image>();
material = localImg.material;
imgWidth = Mathf.FloorToInt(gameObject.GetComponent<RectTransform>().rect.width);
imgHeight = Mathf.FloorToInt(gameObject.GetComponent<RectTransform>().rect.height);
screenPixel = new float[imgWidth, imgHeight];
samplePixel = new float[Mathf.FloorToInt(shapeTex.width), Mathf.FloorToInt(shapeTex.height)];
// 制作一张保存信息的贴图
tempRt = new Texture2D(imgWidth, imgHeight, TextureFormat.RGBA32, false);
tempRt.name = "信息贴图";
ResetErase();
// Get Smaple Pixel
for (int i = 0; i < Mathf.FloorToInt(shapeTex.width); i++)
{
for (int j = 0; j < Mathf.FloorToInt(shapeTex.height); j++)
{
samplePixel[i, j] = shapeTex.GetPixel(i, j).a;
}
}
}
void ResetErase()
{
// 初始化
for (int i = 0; i < imgWidth; i++)
{
for (int j = 0; j < imgHeight; j++)
{
Color color = new Color(0, 0, 0, 0);
tempRt.SetPixel(i, j, color);
screenPixel[i, j] = 0;
}
}
tempRt.Apply();
material.SetTexture("_SampleTex", tempRt);
}
public void OnPointerDown(PointerEventData data){
Vector2 localPos = ScreenPointToLocal(Input.mousePosition);
DrawMask(localPos);
}
public void OnDrag(PointerEventData data)
{
Vector2 localPos = ScreenPointToLocal(Input.mousePosition);
DrawMask(localPos);
}
Vector2 ScreenPointToLocal(Vector3 mousePos)
{
Vector2 outPos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTrans, mousePos, UICam, out outPos))
{
// 建立笛卡尔坐标系
outPos = new Vector2((int)outPos.x + imgWidth / 2, (int)outPos.y + imgHeight / 2);
return outPos;
}
return Vector2.zero;
}
// 绘制消除区域的像素信息
void DrawMask(Vector2 pos)
{
int startX = Mathf.FloorToInt(pos.x - shapeTex.width / 2);
int endX = Mathf.FloorToInt(pos.x + shapeTex.width / 2);
int startY = Mathf.FloorToInt(pos.y - shapeTex.height / 2);
int endY = Mathf.FloorToInt(pos.y + shapeTex.height / 2);
for (int i = startX; i < endX; i++)
{
for (int j = startY; j < endY; j++)
{
if (i < 0 || i >= imgWidth || j < 0 || j >= imgHeight)
{
continue;
}
// 采样图的坐标值
int sampleX = i - startX;
int sampleY = j - startY;
float alpha = samplePixel[sampleX, sampleY];
float a = alpha > screenPixel[i, j] ? alpha : screenPixel[i, j];
Color color = new Color(0, 0, 0, a);
tempRt.SetPixel(i, j, color);
if (alpha > screenPixel[i, j])
{
screenPixel[i, j] = alpha;
}
}
}
tempRt.Apply();
material.SetTexture("_SampleTex", tempRt);
}
}
使用的shader:
Shader "UI/MaskSet"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SampleTex ("Mask Texture " , 2D) = "black" {}
}
SubShader
{
Tags{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
}
Cull Off ZWrite Off ZTest Always
// 混合模式 最终颜色 = 源颜色 * 源a + (1 - a) * 目标颜色
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
// 由脚本传入的像素信息
sampler2D _SampleTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 maskCol = tex2D(_SampleTex, i.uv);
// 取相反值
col.a = 1 - maskCol.a;
return col;
}
ENDCG
}
}
}
在UI上生成一张Image作为遮罩,然后将shade对应的材质球交给Image,运行即可在拖动鼠标看到效果。
测试工程在其他地方上传
C#脚本改了一下,使用线性插值的方式做平滑处理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class MaskErase : MonoBehaviour, IDragHandler, IPointerDownHandler
{
// 取图片的父节点,转成本地坐标
public GameObject parent;
// 相机转行坐标使用
public Camera UICam;
// public int rectScale = 10;
// 控制形状
public Texture2D shapeTex;
Texture2D tempRt;
// 转成本地节点下的坐标下使用
RectTransform rectTrans;
private int imgWidth = 0;
private int imgHeight = 0;
private Material material;
// 数组保留像素值, 记录采样图的数据
float[,] samplePixel;
// 记录遮罩图的数据,判断是否可以替换当前像素的值
float[,] screenPixel;
// 上一次的鼠标位置
Vector2 lastPos = Vector2.zero;
void Start()
{
rectTrans = parent.GetComponent<RectTransform>();
Image localImg = gameObject.GetComponent<Image>();
material = localImg.material;
imgWidth = Mathf.FloorToInt(gameObject.GetComponent<RectTransform>().rect.width);
imgHeight = Mathf.FloorToInt(gameObject.GetComponent<RectTransform>().rect.height);
screenPixel = new float[imgWidth, imgHeight];
samplePixel = new float[Mathf.FloorToInt(shapeTex.width), Mathf.FloorToInt(shapeTex.height)];
// 制作一张保存信息的贴图
tempRt = new Texture2D(imgWidth, imgHeight, TextureFormat.RGBA32, false);
tempRt.name = "信息贴图";
ResetErase();
// Get Smaple Pixel
for (int i = 0; i < Mathf.FloorToInt(shapeTex.width); i++)
{
for (int j = 0; j < Mathf.FloorToInt(shapeTex.height); j++)
{
samplePixel[i, j] = shapeTex.GetPixel(i, j).a;
}
}
}
void ResetErase()
{
// 初始化
for (int i = 0; i < imgWidth; i++)
{
for (int j = 0; j < imgHeight; j++)
{
Color color = new Color(0, 0, 0, 0);
tempRt.SetPixel(i, j, color);
screenPixel[i, j] = 0;
}
}
tempRt.Apply();
material.SetTexture("_SampleTex", tempRt);
}
public void OnPointerDown(PointerEventData data){
Vector2 localPos = ScreenPointToLocal(Input.mousePosition);
DrawMask(localPos);
lastPos = localPos;
}
public void OnDrag(PointerEventData data)
{
Vector2 localPos = ScreenPointToLocal(Input.mousePosition);
// 取形状控制贴图的一半作为是否进行插值的判断
if ((Mathf.Abs(localPos.x - lastPos.x) > (shapeTex.width / 2)))
{
List<Vector2> pointsList = new List<Vector2>();
if ((Mathf.Abs(localPos.y - lastPos.y) > (shapeTex.height / 2)))
{
pointsList = GetInterplotion(lastPos, localPos, true);
}
else
{
pointsList = GetInterplotion(lastPos, localPos, false);
}
for (int i = 0; i < pointsList.Count; i++)
{
DrawMask(pointsList[i]);
}
}
else
{
DrawMask(localPos);
}
lastPos = localPos;
}
// 由于拖动一般是直线,所以可以使用线性插值的方式
List<Vector2> GetInterplotion(Vector2 start, Vector2 end, bool isReverse)
{
List<Vector2> pointsList = new List<Vector2>();
if (isReverse)
{
float k = (end.x - start.x) / (end.y - start.y);
float b = start.x - k * start.y;
// 1 / 3的距离进行插值
for (int i = (int)start.y; i < (int)end.y; i = i + (shapeTex.height / 3))
{
pointsList.Add(new Vector2(i, (int)((i * k) + b)));
}
}
else
{
float k = (end.y - start.y) / (end.x - start.x);
float b = start.y - k * start.x;
// 1 / 3的距离进行插值
for (int i = (int)start.x; i < (int)end.x; i = i + (shapeTex.width / 3))
{
pointsList.Add(new Vector2(i, (int)((i * k) + b)));
}
}
return pointsList;
}
Vector2 ScreenPointToLocal(Vector3 mousePos)
{
Vector2 outPos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTrans, mousePos, UICam, out outPos))
{
// 建立笛卡尔坐标系
outPos = new Vector2((int)outPos.x + imgWidth / 2, (int)outPos.y + imgHeight / 2);
return outPos;
}
return Vector2.zero;
}
// 绘制消除区域的像素信息
void DrawMask(Vector2 pos)
{
int startX = Mathf.FloorToInt(pos.x - shapeTex.width / 2);
int endX = Mathf.FloorToInt(pos.x + shapeTex.width / 2);
int startY = Mathf.FloorToInt(pos.y - shapeTex.height / 2);
int endY = Mathf.FloorToInt(pos.y + shapeTex.height / 2);
for (int i = startX; i < endX; i++)
{
for (int j = startY; j < endY; j++)
{
if (i < 0 || i >= imgWidth || j < 0 || j >= imgHeight)
{
continue;
}
// 采样图的坐标值
int sampleX = i - startX;
int sampleY = j - startY;
float alpha = samplePixel[sampleX, sampleY];
float a = alpha > screenPixel[i, j] ? alpha : screenPixel[i, j];
Color color = new Color(0, 0, 0, a);
tempRt.SetPixel(i, j, color);
if (alpha > screenPixel[i, j])
{
screenPixel[i, j] = alpha;
}
}
}
tempRt.Apply();
material.SetTexture("_SampleTex", tempRt);
}
void OnDestroy()
{
}
}