通过一个小Trick实现shader的像素识别/统计操
1.简介
将一张大图分成多个小块逐步处理并逐步合并,保留关键像素的向下采样:
但我在思考一种更简便的方法,于是想到在顶点shader里做判断检测,在像素shader里获取结果这样一个形式:
用一组顶点去读单个像素,判断失败的顶点坐标提交到屏幕外,而判断成功的顶点坐标放在屏幕内。
最后在CPU中获取是否有屏幕内顶点这样一个结果,来进行简单的识别操作。
而在开启透明之后,还可以用透明度叠加来获取更复杂的结果。
2.实践
首先实践结果并没有想象的那么好,因为如果纯用三角面来做顶点部分的判断未免太费效率了。
所以我改成了传入顶点判断并生成面的方式,并且缩小了传入图片的像素大小。
Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1);
毕竟更多的运用场合是用来做刮刮卡或者擦除的识别。只需要检测mask图片。
上代码:
Shader "Hidden/FooShader"
{
Properties
{
}
SubShader
{
Blend One One
tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
}
Pass
{
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 color : COLOR;
float4 vertex : SV_POSITION;
};
sampler2D _Image;
float4 _ImageSize;
v2f vert(uint vid : SV_VertexID)
{
v2f o = (v2f)0;
half y = floor(vid / _ImageSize.x);
half x = (vid - y * _ImageSize.x) / _ImageSize.x;
y = y / _ImageSize.y;
o.vertex = 0;
float4 image_col = tex2Dlod(_Image, half4(x,y,0,0));
if (all(image_col.rgb == half3(0, 0, 1)))
//if (all(image_col.rgb == half3(0, 1, 1))) /*error*/
{
o.color = 1;
}
else
{
o.color = 0;
}
return o;
}
[maxvertexcount(4)]
void geom(point v2f vertElement[1], inout TriangleStream<v2f> triStream)
{
if (vertElement[0].color.r <= 0) return;
float size = 10;
float4 v1 = vertElement[0].vertex + float4(-size, -size, 0, 0);
float4 v2 = vertElement[0].vertex + float4(-size, size, 0, 0);
float4 v3 = vertElement[0].vertex + float4(size, -size, 0, 0);
float4 v4 = vertElement[0].vertex + float4(size, size, 0, 0);
v2f r = (v2f)0;
r.vertex = mul(UNITY_MATRIX_VP, v1);
r.color = vertElement[0].color;
triStream.Append(r);
r.vertex = mul(UNITY_MATRIX_VP, v2);
r.color = vertElement[0].color;
triStream.Append(r);
r.vertex = mul(UNITY_MATRIX_VP, v3);
r.color = vertElement[0].color;
triStream.Append(r);
r.vertex = mul(UNITY_MATRIX_VP, v4);
r.color = vertElement[0].color;
triStream.Append(r);
}
fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Hont
{
public class Foo : MonoBehaviour
{
void Start()
{
var blueTex = new Texture2D(64, 64);
for (int x = 0; x < blueTex.width; x++)
for (int y = 0; y < blueTex.height; y++)
blueTex.SetPixel(x, y, Color.blue);
blueTex.Apply();
var mat = new Material(Shader.Find("Hidden/FooShader"));
mat.SetTexture("_Image", blueTex);
mat.SetVector("_ImageSize", new Vector4(blueTex.width, blueTex.height));
mat.SetPass(0);
var tempRT = RenderTexture.GetTemporary(16, 16, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB, 1);
tempRT.filterMode = FilterMode.Point;
tempRT.autoGenerateMips = false;
tempRT.anisoLevel = 0;
tempRT.wrapMode = TextureWrapMode.Clamp;
var cacheRT = RenderTexture.active;
RenderTexture.active = tempRT;
Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1);
var tex2D = new Texture2D(16, 16, TextureFormat.ARGB32, false, false);
tex2D.wrapMode = TextureWrapMode.Clamp;
tex2D.anisoLevel = 0;
tex2D.filterMode = FilterMode.Point;
tex2D.ReadPixels(new Rect(0, 0, 16, 16), 0, 0);
var firstPixel = tex2D.GetPixel(0, 0);
Debug.Log("firstPixel: " + firstPixel);
RenderTexture.active = cacheRT;
RenderTexture.ReleaseTemporary(tempRT);
}
}
}
跑了一下代码之后我发现了三个问题,也是没解决的问题,一个是计算结果有误差
o.color = float4(0.05, 0, 0, 0);
输出是0.05结果却有一些出入。
特别是当返回颜色小于0.1之后,我尝试改变图像格式或者RT等参数依旧没能解决
第二个问题是开启透明后,透明图片的叠加是有上限的,毕竟深度有限,堆叠二十多层后,后面层会丢失。
第三个问题是传入图片尺寸过大直接导致带宽爆炸,以至于unity直接假死了,512x512的图片就是26万多的像素要处理,也就是26万多的顶点。
第三个问题很好解决,控制图片尺寸+让单个顶点采样更多像素即可。
对于第一个问题,目前还不需要太精确所以没解决但也能用。第二个问题可以用一些方法来缓解
比如在顶点shader中增加运算量,把返回值分散到rgba四个通道上去。
uint roll = (roll_width + roll_height) % 4;
if (roll == 0)
result = float4(GAIN_VALUE, 0, 0, 0);
if (roll == 1)
result = float4(0, GAIN_VALUE, 0, 0);
if (roll == 2)
result = float4(0, 0, GAIN_VALUE, 0);
if (roll == 3)
result = float4(0, 0, 0, GAIN_VALUE);
把更多的像素遍历放入顶点中,这样处理图片的顶点数量是原大小/n:
v2f vert(uint vid : SV_VertexID)
{
v2f o = (v2f)0;
o.vertex = 0;
half2 image_size = half2(GRID_SIZE_X * LOOP_IMAGE_SIZE_X, GRID_SIZE_Y * LOOP_IMAGE_SIZE_Y);
half y = floor(vid / LOOP_IMAGE_SIZE_X);
half x = (vid - y * LOOP_IMAGE_SIZE_X) / LOOP_IMAGE_SIZE_X;
y = y / LOOP_IMAGE_SIZE_Y;
//将vid转化为x,y坐标
for (half rx = 0; rx < GRID_SIZE_X; rx++)
{
for (half ry = 0; ry < GRID_SIZE_Y; ry++)
{
half xx = x + rx;
half yy = y + ry;
float4 r = Statistics_sample(_Image, _Rec_Color, half4(xx, yy, 0, 0), image_size);
o.color += r;
}
}
//一个顶点处理多个像素
return o;
}
3.测试结果
我写了一个涂抹效果demo来测试一下,它通过识别白色像素的数量来判断是否为全部涂完:
工程文件我丢在了github上: https://github.com/hont127/Image-Rec-Base-unity-shader-
UGUI 中通过改变像素实现擦除
这种方法需要改精灵的设置,如下:
话不多说,直接上代码:
using System.Collections.Generic;
using System.Reflection.Emit;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Profiling;
using UnityEngine.UI;
public class ChangeTexturePixel : MonoBehaviour, IDragHandler
{
/// <summary> 擦除的像素数量 </summary>
private int m_PixelAcount = 0;
/// <summary> 是否擦除成功 </summary>
private bool m_IsDrag = false;
/// <summary> 擦除范围大小 </summary>
[SerializeField][Range(10,100)]
private int Radius = 10;
/// <summary> 擦除完成度(不超过1)</summary>
[SerializeField][Range(0,1)]
private float m_Complete;
private RawImage m_UITex;
private Texture2D m_MyTex;
[SerializeField]
private Color m_Col = Color.clear;
private int[][] m_PixelArray;
private Dictionary<int, TexturePixel> m_TexPixelDic = new Dictionary<int, TexturePixel>();
void Start()
{
m_IsDrag = false;
m_UITex = GetComponent<RawImage>();
var tex = m_UITex.texture as Texture2D;
m_MyTex = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32,
false);
m_MyTex.SetPixels(tex.GetPixels());
m_MyTex.Apply();
m_UITex.texture = m_MyTex;
int value = 0;
m_PixelArray = new int[m_MyTex.width][];
for (int i = 0; i < m_PixelArray.Length; i++)
{
m_PixelArray[i] = new int[m_MyTex.height];
for (int j = 0; j < m_MyTex.height; j++)
{
m_PixelArray[i][j] = value;
m_TexPixelDic.Add(value, new TexturePixel(m_MyTex, i, j));
value++;
}
}
}
/// <summary>
/// 改变Texture2D像素点颜色
/// </summary>
/// <param name="x">Texture2D像素点X轴位置</param>
/// <param name="y">Texture2D像素点Y轴位置</param>
/// <param name="radius">改变像素的范围</param>
/// <param name="col">改变后的颜色</param>
void ChangePixelColorByCircle(int x, int y, int radius, Color col)
{
for (int i = -Radius; i < Radius; i++)
{
var py = y + i;
if (py < 0 || py >= m_MyTex.height)
{
continue;
}
for (int j = -Radius; j < Radius; j++)
{
var px = x + j;
if (px < 0 || px >= m_MyTex.width)
{
continue;
}
if (new Vector2(px - x, py - y).magnitude > Radius)
{
continue;
}
Profiler.BeginSample("text1");
TexturePixel tp; //= texPixelDic[pixelArray[MyTex.width - 1][py]];
if (px == 0)
{
tp = m_TexPixelDic[m_PixelArray[m_MyTex.width - 1][py]];
tp.Scratch(m_Col);
}
tp = m_TexPixelDic[m_PixelArray[px][py]];
if (!tp.GetPixel())
{
m_PixelAcount++;
}
tp.Scratch(m_Col);
Profiler.EndSample();
}
}
Profiler.BeginSample("text2");
m_MyTex.Apply();
Profiler.EndSample();
Profiler.BeginSample("text3");
Profiler.EndSample();
}
/// <summary>
/// 擦除点
/// </summary>
/// <param name="mousePos">鼠标位置</param>
/// <returns>擦除点</returns>
Vector2 ScreenPoint2Pixel(Vector2 mousePos)
{
float imageWidth = m_UITex.rectTransform.sizeDelta.x;
float imageHeight = m_UITex.rectTransform.sizeDelta.y;
Vector3 imagePos = m_UITex.rectTransform.anchoredPosition3D;
//求鼠标在image上的位置
float HorizontalPercent =
(mousePos.x - (Screen.width / 2 + imagePos.x - imageWidth / 2)) / imageWidth; //鼠标在Image 水平上的位置 %
float verticalPercent =
(mousePos.y - (Screen.height / 2 + imagePos.y - imageHeight / 2)) / imageHeight; //鼠标在Image 垂直上的位置 %
float x = HorizontalPercent * m_MyTex.width;
float y = verticalPercent * m_MyTex.height;
return new Vector2(x, y);
}
/// <summary>
/// 拖拽中。。。
/// </summary>
/// <param name="eventData">拖拽数据</param>
public void OnDrag(PointerEventData eventData)
{
if (!m_IsDrag)
{
var posA = ScreenPoint2Pixel(eventData.position);
ChangePixelColorByCircle((int) posA.x, (int) posA.y, Radius, m_Col);
SetAllPixelFadeAlpha();
}
}
/// <summary>
/// 擦除完成时调用
/// </summary>
public void SetAllPixelFadeAlpha()
{
if (++m_PixelAcount >= m_MyTex.height*m_MyTex.width*m_Complete)
{
m_UITex.color = Color.clear;
m_IsDrag = true;
Debug.Log("擦除完成");
}
}
}
sing System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TexturePixel
{
public Texture2D myTex;
//float alpha = 1; //当前透明度
// int scratchedTime = 0;//被刮的次数
private int x; //像素坐标X
private int y; //像素坐标Y
//private bool scratcedPrevious = false;
//private bool scratcedCurrent = false;
public TexturePixel(Texture2D tex,int x,int y)
{
myTex = tex;
this.x = x;
this.y = y;
}
public void Scratch( Color targetCol)
{
myTex.SetPixel(x,y,targetCol);
// scratcedCurrent = true;
//Debug.Log("x:"+x+" y:"+y+" a "+ targetCol.a);
}
public bool GetPixel()
{
Color color = myTex.GetPixel(x, y);
return color.a <= 0;
}
以上方法是通过改变Texture2D像素点颜色实现擦除,主要内容:
// 设置像素点
myTex.SetPixel(x,y,targetCol);
// 获取像素点
myTex.GetPixel(x,y,targetCol);
项目文件我放在了gitee: https://gitee.com/ondaly/eraser_-master.git
项目案例查看。