Unity 中实现擦除功能

个人博客查看。
原文查看。


通过一个小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
项目案例查看。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Unity实现黑板擦擦除的过程可以通过插值来实现。根据引用\[1\]的描述,当黑板擦的心点与画板的距离是黑板擦长边的0.5时开始插值,当距离为0.02m时结束。根据引用\[2\]的描述,无论黑板擦以何种旋转角度靠近黑板,最终黑板擦的transform.up指向画板的-transform.forward方向。因此,我们可以根据靠近的距离来插值黑板擦的旋转角度。 在Unity,可以通过遍历像素并将其放入顶点来处理图片的顶点数量。引用\[3\]的代码展示了一个处理多个像素的顶点函数。在这个函数,通过遍历GRID_SIZE_X和GRID_SIZE_Y的范围,将顶点的x和y坐标转化为像素的坐标,并使用Statistics_sample函数获取像素的颜色。最后,将获取的颜色累加到顶点的颜色。 综上所述,Unity实现黑板擦擦除的过程可以通过插值黑板擦的旋转角度和遍历像素来实现。 #### 引用[.reference_title] - *1* *2* [使用Unity实现VR在黑板上写字(升级篇)(二)----- 加入黑板擦](https://blog.csdn.net/weixin_30846599/article/details/97959163)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [通过一个小Trick实现shader的像素识别/统计操作](https://blog.csdn.net/dongfushu7972/article/details/102281283)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值