Unity 中实现截图画笔橡皮擦工具
好久没写博客了,随着疫情的缓和,工作也忙了起来 ,写博客也成了忙里偷闲的一项娱乐活动了,不以娱乐为目的技术博客写手不是一个好的厨师(我一向自称自己是码农界最帅的厨子)~
今天就来分享下之前项目中用shader实现的一个画笔橡皮擦功能,当时是一位老师提出要在软件里做笔记的一个需求,希望可以在软件使用过程中划重点,做笔记,再截图保存,方便学生复习,真不愧是一位一切以学生为中心的好老师~
刚接到这个需求,我还在UnityAssetsStore上找了找相关插件,想着直接拿来用多好,找了一些,觉得都不太方便集成到项目, 索性自己用shader写一个吧,也复习一下之前的shader技术。
实现思路:在UI最上层添加一个填充满屏幕的画布RenderTexture, 运用 屏幕后处理技术Graphics.Blit,对当前的画布上的RenderTexture进行笔刷采样处理,画笔效果如下:
核心代码:
private void Paint(Vector2 point)
{
if (point.x < _leftDownConnerPoint.x || point.x > _rightUpConnerPoint.x || point.y < _leftDownConnerPoint.y || point.y > _rightUpConnerPoint.y)
return;
Vector2 uv = new Vector2(point.x / (float)_screenWidth,
point.y / (float)_screenHeight);
if (_eraserFlag)
{
_eraserBrushMat.SetVector("_UV", uv);
Graphics.Blit(_blitRenderTexture, _blitRenderTexture, _eraserBrushMat);
}
else
{
_paintBrushMat.SetVector("_UV", uv);
Graphics.Blit(_blitRenderTexture, _blitRenderTexture, _paintBrushMat);
}
}
point即为我们当前鼠标屏幕坐标,第一句if是我限制了截图可画的区域,重要的是下面的代码,先算出当前采样纹理UV值,然后传入_paintBrushMat的画笔材质shader,在用 Graphics.Blit 去做效果叠加处理。接下来我们看下核心的_paintBrushMat的画笔材质shader代码:
Shader "Unlit/PaintBrush"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BrushTex("Brush Texture",2D)= "white" {}
_Color("Color",Color)=(1,1,1,1)
_UV("UV",Vector)=(0,0,0,0)
_Size("Size",Range(1,1000))=1
_SizeY("SizeY",Range(1,1000))=1
}
SubShader
{
Tags { "RenderType"="Transparent" }
LOD 100
ZTest Always Cull Off ZWrite Off Fog{ Mode Off }
Blend SrcAlpha OneMinusSrcAlpha
//Blend One DstColor
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;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BrushTex;
fixed4 _UV;
float _Size;
float _SizeY;
fixed4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float size = _Size;
float2 uv = i.uv + (0.5f / size);
uv = uv - _UV.xy;
uv *= size;
float sizeY = _SizeY;
uv.y=uv.y*sizeY/size;
fixed4 col = tex2D(_BrushTex,uv);
col.rgb = 1;
col *= _Color;
return col;
}
ENDCG
}
}
}
_BrushTex就是我们的笔刷图标图片,就是个效果图中的那个小黑点图片,_Color就是笔刷颜色,_UV就是我们当前要采样的笔刷点在画布上的UV值,_Size就是我们的画布像素与笔刷图片的像素比例大小,_SizeY是画布高度与画布跨度的比例,计算方式 float sizeY = ((float)Screen.height / (float)Screen.width) * size;(其实不需要size,shader里就不要在size了)因为我们最终的采样结果应该是一个规则圆形,而非宽高不一样的椭圆,我们主要看片源着色器的代码,这里
float2 uv = i.uv + (0.5f / size);
uv = uv - _UV.xy;
uv = size;
其实是做了一个从画布每个UV值到采样图片UV值的一个坐标映射计算,
float sizeY = _SizeY;
uv.y=uv.ysizeY/size;
这里就是我们上面说的要调整uv的x,y采样比例,无论屏幕宽高比例如何,我们都输出圆形。
注意我们要开启深度测试为Always,避免片元被舍弃,关闭深度写入 ,alpha混合设置为Blend SrcAlpha OneMinusSrcAlpha;
我们再来看看橡皮擦得效果,如图(别问我为啥用这次绿色展示,大家都不喜欢绿色,可不得擦掉它嘛,哈哈):
代码如下:
Shader "Unlit/EraserBrush"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BrushTex("Brush Texture",2D)= "white" {}
_Color("Color",Color)=(1,1,1,1)
_UV("UV",Vector)=(0,0,0,0)
_Size("Size",Range(1,1000))=1
_SizeY("SizeY",Range(1,1000))=1
}
SubShader
{
Tags { "RenderType"="Transparent" }
LOD 100
ZTest Always Cull Off ZWrite Off Fog{ Mode Off }
Blend Off
//Blend One DstColor
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;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BrushTex;
fixed4 _UV;
float _Size;
float _SizeY;
fixed4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float size = _Size;
float2 uv = i.uv + (0.5f / size);
uv = uv - _UV.xy;
uv *= size;
float sizeY = _SizeY;
uv.y=uv.y*sizeY/size;
float cirle = pow(uv.x-0.5f, 2)+pow(uv.y-0.5f*sizeY/size, 2);
if(cirle>0.25f)
discard;
fixed4 col = tex2D(_BrushTex,uv);
col.rgb = 1;
col *= _Color;
return col;
}
ENDCG
}
}
}
橡皮擦,我们采用透明空心圆圈的采样图片, 在片元着色器代码处理颜色时不同, float cirle = pow(uv.x-0.5f, 2)+pow(uv.y-0.5f*sizeY/size, 2);
if(cirle>0.25f)
discard;
我们关闭了alpha blend ,在需要擦除的区域之外的片元直接舍弃,这样圆圈空心区域的颜色就直接成了透明,实现了擦除的效果(在shader里用if语句不太好,有待改进)。
需要注意的就是我们在对笔刷采样应该增大它的采样频率,让它在每一帧多采样几次。
本人在shader编程方面的知识也是比较薄弱,欢迎留言指正,谢谢~
附上工程源码:
工程源码:https://github.com/ivoslove/ScreenShotPainter