笔刷效果的实现
最终效果:
片元着色器代码:
Shader "Hidden/Brush"
{
Properties
{
_MainTex("MainTex",2D)="white"{}
_CenterX("CenterX",int) = 0
_CenterY("CenterY",int) = 0
_Radius("Radius",int) = 30
}
SubShader
{
Tags {
"Queue" = "Transparent"
"RenderType" = "Transparent"
}
Blend SrcAlpha OneMinusSrcAlpha
// No culling or depth
Cull Off ZWrite Off ZTest Always
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;
half _CenterX;
half _CenterY;
half _Radius;
half4 _MainTex_TexelSize;
fixed4 frag(v2f i) : SV_Target
{
fixed4 col;
half2 pixelPos = half2(i.uv.x*_MainTex_TexelSize.z,i.uv.y*_MainTex_TexelSize.w);
half2 dis = pixelPos - half2(_CenterX,_CenterY);
col = tex2D(_MainTex, i.uv);duzi
clip(col.a - 0.5);
if (sqrt(dis.x*dis.x + dis.y*dis.y) < _Radius)
col.a = 0.49; //unity的renderTexture有个问题,就是用透明的RGBA贴图设置的时候,
//它会将所有的透明通道信息单独拿出来全部做个乘法叠加,
//depth only的渲染机制导致,会分开存储透明信息。
return col;
}
ENDCG
}
}
}
脚本代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering;
public class control : MonoBehaviour
{
private Material _Mat; //要使用的材质
private RectTransform rt; //要被擦除的ui
public Texture _Texture; //ui绘制的贴图
private RenderTexture _After; //为材质提供的实时贴图
private RenderTexture _Before; //过渡贴图变量
void Start()
{
rt = GetComponent<RectTransform>();
_Mat = new Material(Shader.Find("Hidden/Brush"));
rt.GetComponent<Image>().material = _Mat;
_After = new RenderTexture((int)rt.rect.width, (int)rt.rect.height, 0, RenderTextureFormat.ARGB32);
_Before = new RenderTexture((int)rt.rect.width, (int)rt.rect.height, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(_Texture, _After);
Graphics.Blit(_Texture, _Before);
_Mat.SetTexture("_MainTex", _After); //初始绘制贴图
}
private void Update()
{
if (Input.GetMouseButton(0))
{
var pos = Vector2.zero;
//获取相对于该贴图的像素纹理坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, Input.mousePosition, null, out pos);
pos = pos + new Vector2(rt.rect.width, rt.rect.height) / 2;
_Mat.SetInt("_CenterX", (int)pos.x);
_Mat.SetInt("_CenterY", (int)pos.y);
Graphics.Blit(_Before, _After, _Mat);
Graphics.Blit(_After, _Before);
}
}
//绘制成功可以将_After拷贝到texture2D上,通过遍历像素点的透明度来判断贴图是否擦除完成
//成功后通过Release()方法来释放_After/_Before空间资源
}
使用着色器来实现刷子效果主要要解决的问题以及解决方法:
问题:
- 如何使用脚本将鼠标位置映射到要被擦除的贴图的纹理坐标 。
- 如何保存下来每一次操作后的贴图以便于下一次在上一次操作的基础上继续操作。
- 如何在Shader其中将笔刷范围内的像素值的透明度设置为0,即全透明。
- 因为传入Shader中的贴图是RenderTexture类型的,所以如何将Render Texture的某些像素点设置为全透明。
方法:
var pos = Vector2.zero;
//获取相对于该贴图的像素纹理坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, Input.mousePosition, null, out pos);
pos = pos + new Vector2(rt.rect.width, rt.rect.height) / 2;
rt就是要被擦出的UI,可以通过 RectTransformUtility.ScreenPointToLocalPointInRectangleAPI来将屏幕坐标系下的鼠标 位置转换为该UI纹理下的像素坐标(即左下角为原点(0,0),右上角为(width,height))。
我们可以在程序初始化时,将RenderTexture类型的_After绑定到刷子Shader的_MainTex属性上,在程序运行中,我们只需要改变_After,刷子Shader对应处理的_MainTex也会发生变化。
_Mat.SetTexture("_MainTex", _After); //初始绘制贴图
需要将每次修改过后的_After保留下来以便下次继续在上次操作的基础上继续进行操作。所以我们需要一个RenderTexture类型的过渡变量_Before;
Graphics.Blit(_Before, _After, _Mat);
Graphics.Blit(_After, _Before);
本来想的时将像素透明度设置为0(col.a=0),就可以让像素透明。但是后来遇到了一个坑如果将col.a设置为0的话,上一次的操作就不会保留下来。也就是说将RenderTexture的像素透明度设置为0,在RenderTexture中存储时透明度并不是简单的存储为0。unity的renderTexture有个问题,透明的RGBA贴图设置的时候,它会将所有的透明通道信息单独拿出来全部做个乘法叠加,depth only的渲染机制导致,会分开存储透明信息。所以这里并不是简单的将像素点透明度设置为0就可以了。可以使用像素剔除的方法,也就是说把低于特定透明度的像素直接丢弃掉。
当我设置RenderTexture的透明度时发现,a=0.5是半透明,a=0||a=1都是不透明。所以我的思路是将我要剔除的区域的透明度设置为0.49,只要把透明度低于0.5的像素点剔除掉就可以实现擦出的效果。
clip(col.a - 0.5);
if (sqrt(dis.x*dis.x + dis.y*dis.y) < _Radius)
col.a = 0.49;
//unity的renderTexture有个问题,就是用透明的RGBA贴图设置的时候,
//它会将所有的透明通道信息单独拿出来全部做个乘法叠加,
//depth only的渲染机制导致,会分开存储透明信息。
return col;
更新
**************************************************************************************
上一次实现笔刷效果时,因为时间原因没有完善笔刷中间擦涂不干净的问题
下面是最近更新
**************************************************************************************
问题:
首先,擦除不干净的问题,应该是当鼠标移动过快时,在Update函数中向Shader传送的两个点距离太远,笔刷之擦除鼠标位置点半径范围之内的像素,所以中间区域就漏掉。
解决思路:
直接细分上次鼠标位置和本次鼠标位置,然后根据细分返回的点逐点擦除。
我主要使用直线算法来进行点的细分,也可以使用贝兹曲线。主要是我觉得如果项目不会让帧率很低的话,使用直线算法来进行细分已经可以了,效果差别不大。如果想使用贝兹曲线来生成点的话,也是可以的。
private Vector2 _LastPos;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
_TwoPoints = true;
_LastPos = Input.mousePosition;
}
else if (Input.GetMouseButton(0))
{
var currentPos = Input.mousePosition;
if (Vector2.Distance(_LastPos, currentPos) > 30)
{
int segments = (int)(Vector2.Distance(_LastPos, currentPos) / 10.0F);
var points = GetPoints(_LastPos, currentPos, segments);
Draw(points);
_LastPos = Input.mousePosition;
}
}
if (Input.GetMouseButtonUp(0))
{
if (PixelDetection())
Debug.Log("Complete");
else
Debug.Log("No Complete");
}
}
/// <summary>
/// 细分
/// </summary>
/// <param name="lastPos"></param>
/// <param name="endPos"></param>
/// <param name="segments"></param>
/// <returns></returns>
public Vector2[] GetPoints(Vector2 lastPos, Vector2 endPos, float segments)
{
// Debug.Log(segments);
Vector2[] points = new Vector2[(int)segments];
for (int i = 0; i < (int)segments; i++)
{
points[i] = ((endPos - lastPos) / segments) * i + lastPos;
// Debug.Log(points[i]);
}
return points;
}
/// <summary>
/// 根据细分出的点来逐个擦除
/// </summary>
/// <param name="points"></param>
public void Draw(Vector2[] points)
{
for (int i = 0; i < points.Length; i++)
{
var pos = Vector2.zero;
//获取相对于该贴图的像素纹理坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, points[i], null, out pos);
pos = pos + new Vector2(rt.rect.width, rt.rect.height) / 2;
_Mat.SetInt("_CenterX", (int)pos.x);
_Mat.SetInt("_CenterY", (int)pos.y);
Graphics.Blit(_Before, _After, _Mat);
Graphics.Blit(_After, _Before);
}
}
/// <summary>
/// 像素检测,是否擦除干净
/// </summary>
/// <returns></returns>
public bool PixelDetection()
{
var currentRT = RenderTexture.active;
RenderTexture.active = _After;
_TempTexture.ReadPixels(new Rect(0, 0, _After.width, _After.height), 0, 0);
RenderTexture.active = currentRT;
var number = 0;
for (int i = 0; i < _TempTexture.width; i++)
{
for (int j = 0; j < _TempTexture.height; j++)
{
var a = _TempTexture.GetPixel(i,j).a;
if (a < 0.9F)
number++;
}
}
var count = _TempTexture.width * _TempTexture.height;
var percentage = (float)number / (float)count;
if (percentage > 0.95)
return true;
else
return false;
}
Demo地址:https://github.com/RXBXX/Brush
最后多谢https://gameinstitute.qq.com/community/user/1054734feng,解决了我关于renderTexture透明度存储的问题。希望可以帮到有类似需求的读者。希望在交流学习中成长。