Unity刮刮乐效果实现

Unity UI 实现类似刮刮乐的刮擦效果(可自定义笔刷材质)

1.ScratchCard.shader

1
2
3
4
5
6
7
Properties
{
_MainTex ("Brush Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
[PerRendererData] _Offset ("Offset", Vector) = (0,0,0,0)
[PerRendererData] _Scale ("Scale", Vector) = (0,0,0,0)
}
  • Brush Texture 笔刷贴图
  • Offset/Scale 分别存偏移和缩放
  • PerRendererData 属性标记,指示该属性可以在每个渲染器实例上设置不同的值而不用为每个对象创建独立的材质实例
1
2
3
4
5
6
7
8
9
10
11
Cull Off
Lighting Off
ZWrite Off
BlendOp RevSub
Blend Zero One, One One

举一个混合的例子
源颜色为(0.8, 0.5, 0.2, 1.0)
目标颜色为(0.3, 0.4, 0.5, 1.0)
经过混合后为(-0.5, -0.1, 0.3, 0.0)
颜色值在[0,1] 所以结果为(0.0,0.0,0.3,0.0)
  • Cull Off 关闭背面剔除
  • Lighting Off 关闭光照计算,UI不参与光照计算
  • ZWrite Off 关闭深度写入,透明对象需要正确的渲染顺序,如果深度写入打开,可能会导致透明对象在渲染时遮挡后面的物体
  • BlendOp RevSub 反向减法混合,目标颜色=目标颜色(帧缓冲区中的颜色)-源颜色(正在处理的片元颜色)
  • Blend Zero One, One One 混合时源颜色RGB不参与A参与,目标颜色RGBA都参与
1
2
3
4
5
6
7
8
9
10
v2f vert(a2v v)
{
v2f o;

o.vertex = UnityObjectToClipPos(v.vertex + _Offset);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex) * _Scale.xy;
o.color = v.color * _Color;

return o;
}
  • 模型空间应用偏移后UnityObjectToClipPos转换到裁剪空间
  • TRANSFORM_TEX应用材质贴图平移和缩放后再进行一次缩放
1
2
3
4
5
fixed4 frag(v2f i) : SV_Target
{
half4 color = tex2D(_MainTex, i.texcoord) * i.color;
return color;
}
  • tex2D采样纹理颜色,与顶点着色器输出的颜色进行混合后输出

2.ScratchCard.cs

1
public class ScratchCard : MonoBehaviour, IBeginDragHandler, IDragHandler
  • 实现IBeginDragHandler,IDragHandler接口
1
2
3
4
5
6
7
8
private void DrawAt(Vector2 screenPoint)
{
Vector2 point = ScreenToGraphicLocalPoint(RawImage, screenPoint);
Vector4 offsetScale = CaculateOffsetScale(RectTransform, point, brushScale);
brush.SetVector(OffsetPropertyId, new Vector4(offsetScale.x, offsetScale.y));
brush.SetVector(ScalePropertyId, new Vector4(offsetScale.z, offsetScale.w));
Graphics.Blit(brush.mainTexture, targetTexture, brush);
}
  • screenPoint为鼠标坐标
  • ScreenToGraphicLocalPoint方法将鼠标坐标转换到RawImage空间
  • CaculateOffsetScale方法根据坐标和笔刷大小计算偏移和缩放
  • 将计算得到的值赋给材质对应属性后Graphics.Blit方法应用材质将笔刷贴图绘制到targetTexture实现效果
1
2
3
4
5
6
7
8
9
10
11
12
13
private Vector4 CaculateOffsetScale(RectTransform rectTransform, Vector2 localPoint, Vector2 brushScale)
{
Rect rect = rectTransform.rect;
localPoint += rectTransform.pivot * rect.size;

float aspect = rect.height / rect.width;
float sx = brushScale.x * aspect;
float sy = brushScale.y;
float px = (localPoint.x / rect.width) - (0.5f * sx);
float py = (localPoint.y / rect.height) - (0.5f * sy);

return new Vector4(px, py, 1.0f / sx, 1.0f / sy);
}
  • localPoint是以纹理中心为原点的,需要转化到左下角为原点(OpenGL以左下角为uv原点)
  • sx需要乘以aspect保持宽高比
  • 这里先将localPoint归一化,乘以 0.5f 是为了将画笔的中心对准 localPoint,而不是默认的左下角。这相当于将画笔的起始位置从左下角调整到中心,这样绘制时画笔的中心会与 localPoint 对齐

3.刮开一定比例后Dissolve

实现比较简单,需要维护一个结构体,记录访问过的点位,访问的比例到达设置值后触发溶解效果即可

4.完整代码

  1. ScratchCard.shader
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    Shader "UI/ScratchCard/Brush"
    {
    Properties
    {
    _MainTex ("Brush Texture", 2D) = "white" {}
    _Color ("Tint", Color) = (1,1,1,1)
    [PerRendererData] _Offset ("Offset", Vector) = (0,0,0,0)
    [PerRendererData] _Scale ("Scale", Vector) = (0,0,0,0)
    }

    SubShader
    {
    Tags
    {
    "Queue"="Transparent"
    "RenderType" = "Transparent"
    "IgnoreProjector"="True"
    "PreviewType"="Plane"
    "CanUseSpriteAtlas"="True"
    }

    Cull Off
    Lighting Off
    ZWrite Off
    BlendOp RevSub
    Blend Zero One, One One

    Pass
    {
    Name "Default"
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 2.0

    #include "UnityCG.cginc"

    struct a2v
    {
    float4 vertex : POSITION;
    float4 color : COLOR;
    float2 texcoord : TEXCOORD0;
    };

    struct v2f
    {
    float4 vertex : SV_POSITION;
    float4 color : COLOR;
    float2 texcoord : TEXCOORD0;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float4 _Color;
    float4 _Offset;
    float4 _Scale;

    v2f vert(a2v v)
    {
    v2f o;

    o.vertex = UnityObjectToClipPos(v.vertex + _Offset);
    o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex) * _Scale.xy;
    o.color = v.color * _Color;

    return o;
    }

    fixed4 frag(v2f i) : SV_Target
    {
    half4 color = tex2D(_MainTex, i.texcoord) * i.color;
    return color;
    }
    ENDCG
    }
    }
    }
  2. ScratchCard.cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    using UnityEngine;
    using UnityEngine.EventSystems;
    using UnityEngine.UI;

    [RequireComponent(typeof(RawImage))]
    [RequireComponent(typeof(RectTransform))]
    public class ScratchCard : MonoBehaviour, IBeginDragHandler, IDragHandler
    {
    private static readonly int OffsetPropertyId = Shader.PropertyToID("_Offset");
    private static readonly int ScalePropertyId = Shader.PropertyToID("_Scale");

    internal RawImage RawImage
    {
    get
    {
    if (rawImage == null)
    {
    rawImage = GetComponent<RawImage>();
    }
    return rawImage;
    }
    }

    internal RectTransform RectTransform
    {
    get
    {
    if (rectTransform == null)
    {
    rectTransform = GetComponent<RectTransform>();
    }
    return rectTransform;
    }
    }

    [Header("Brush")]
    public Material brushMaterial;
    public Vector2 brushScale = new Vector2(0.25f, 0.25f);
    public int interpolationMaxCount = 4;
    public int interpolationMinDistance = 8;

    [Header("MaskTexture")]
    public float maskTextureScale = 10f;
    public float maskTextureMaxSize = 1024f;

    private RawImage rawImage;
    private RectTransform rectTransform;
    private Texture baseTexture;
    private RenderTexture targetTexture;
    private Material brush;
    private Vector2 previousDrawPosition;

    private void Awake()
    {
    SaveBaseTexture();
    }

    private void OnEnable()
    {
    Reload();
    }

    private void OnDisable()
    {
    DisposeTargetTexture();
    DisposeBrush();
    }

    private void OnRectTransformDimensionsChange()
    {
    Reload();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
    previousDrawPosition = eventData.position;
    }

    public void OnDrag(PointerEventData eventData)
    {
    if (targetTexture == null || brush == null) return;

    Vector2 drawPositon = eventData.position;
    Vector2 delta = drawPositon - previousDrawPosition;
    float distance = delta.magnitude;
    int count = Mathf.Min(interpolationMaxCount, (int)distance / interpolationMinDistance);

    for(int i = 1; i < count; i++)
    {
    DrawAt(previousDrawPosition + delta * ((float)i / count));
    }
    DrawAt(drawPositon);
    previousDrawPosition = drawPositon;
    }

    public void Restory()
    {
    ClearTargetTexture();
    }

    private void SaveBaseTexture()
    {
    if (RawImage.texture != null)
    {
    baseTexture = RawImage.texture;
    }
    }

    private void Reload()
    {
    Rect rect = RectTransform.rect;
    if (rect.width <= 0 || rect.height <= 0) return;

    AcquireTargetTexture(rect);
    CreateBrush();
    }

    private void AcquireTargetTexture(Rect rect)
    {
    float aspect = rect.height / rect.width;
    int width = (int)Mathf.Min(maskTextureMaxSize, rect.width * maskTextureScale);
    int height = (int)(width * aspect);

    if (targetTexture != null && (targetTexture.width != width || targetTexture.height != height))
    {
    DisposeTargetTexture();
    }

    targetTexture = RenderTexture.GetTemporary(width, height, -1, RenderTextureFormat.ARGB32);
    RawImage.texture = targetTexture;
    ClearTargetTexture();
    }

    private void DisposeTargetTexture()
    {
    if(targetTexture != null)
    {
    RenderTexture.ReleaseTemporary(targetTexture);
    targetTexture = null;
    }
    }

    private void ClearTargetWithColor(Color color)
    {
    RenderTexture temp = RenderTexture.active;
    RenderTexture.active = targetTexture;
    GL.Clear(true, true, color);
    RenderTexture.active = temp;
    }

    private void ClearTargetTexture()
    {
    if(baseTexture != null)
    {
    ClearTargetWithColor(Color.clear);
    Graphics.Blit(baseTexture, targetTexture, RawImage.material);
    }
    else
    {
    ClearTargetWithColor(Color.white);
    }
    }

    private void CreateBrush()
    {
    if(brushMaterial == null)
    {
    brushMaterial = Resources.Load<Material>("Materials/ScratchCard_DefaultBrush");
    }

    DisposeBrush();

    brush = new Material(brushMaterial);
    }

    private void DisposeBrush()
    {
    if(brush != null)
    {
    Destroy(brush);
    brush = null;
    }
    }

    private void DrawAt(Vector2 screenPoint)
    {
    // 这里计算得到的坐标以rect中心为原点
    Vector2 point = ScreenToGraphicLocalPoint(RawImage, screenPoint);
    Vector4 offsetScale = CaculateOffsetScale(RectTransform, point, brushScale);
    brush.SetVector(OffsetPropertyId, new Vector4(offsetScale.x, offsetScale.y));
    brush.SetVector(ScalePropertyId, new Vector4(offsetScale.z, offsetScale.w));
    Graphics.Blit(brush.mainTexture, targetTexture, brush);
    }

    private Vector2 ScreenToGraphicLocalPoint(Graphic graphic, Vector2 screenPoint)
    {
    Canvas canvas = graphic.canvas.rootCanvas;
    Camera camera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
    RectTransform rectTransform = graphic.rectTransform;

    return RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, camera, out Vector2 localPoint) ? localPoint : default;
    }

    private Vector4 CaculateOffsetScale(RectTransform rectTransform, Vector2 localPoint, Vector2 brushScale)
    {
    Rect rect = rectTransform.rect;
    localPoint += rectTransform.pivot * rect.size;

    float aspect = rect.height / rect.width;
    float sx = brushScale.x * aspect;
    float sy = brushScale.y;
    float px = (localPoint.x / rect.width) - (0.5f * sx);
    float py = (localPoint.y / rect.height) - (0.5f * sy);

    return new Vector4(px, py, 1.0f / sx, 1.0f / sy);
    }
    }

Unity刮刮乐效果实现
https://baifabaiquan.cn/2024/08/13/Unity刮刮乐效果实现/
作者
白发败犬
发布于
2024年8月13日
许可协议