一、插件核心架构

这套插件一共分 4 层,完全对应你提供的脚本:

  1. 交互层ScratchCardInput.cs —— 处理触摸 / 鼠标
  2. 数据层BaseData / ImageData / SpriteRendererData / MeshRendererData —— 坐标→UV 计算
  3. 渲染层ScratchCardRenderer.cs —— 往 RenderTexture 画刮痕
  4. 显示层ScratchCard.cs + Shader —— 用 RT 做遮罩显示隐藏
  5. 辅助层EraseProgress.cs —— 计算刮开百分比

二、核心原理

插件不修改 Sprite 原始 UV,而是:通过屏幕坐标 → 局部坐标 → 精确 UV 映射,在RenderTexture上绘制刮痕,再通过Shader 遮罩控制刮层显示 / 隐藏。


三、核心脚本逐行解析

1. ScratchCard.cs(总控制脚本)

作用: 插件的大脑,管理 RT、输入、渲染。关键代码:

csharp

运行

// 创建渲染纹理 RT
private void CreateRenderTexture()
{
    RenderTexture = new RenderTexture(..., renderTextureFormat);
    // 把 RT 传给 Shader 当遮罩
    SurfaceMaterial.SetTexture(Constants.MaskShader.MaskTexture, RenderTexture);
}

// 接收输入,调用渲染
private void TryScratchHole(Vector2 position, float pressure)
{
    cardRenderer.ScratchHole(position, pressure);
}

2. ScratchCardRenderer.cs(真正画刮痕的地方)

作用: 使用 CommandBuffer 往 RT 画四边形(刮痕)。关键代码:

csharp

运行

// 画一个刮痕(四边形)
public void ScratchHole(Vector2 position, float pressure = 1f)
{
    // 计算在 RT 上的矩形位置
    var positionRect = new Rect(...)
    
    // 设置 Mesh 顶点
    meshHole.vertices = new[]
    {
        new Vector3(positionRect.xMin, positionRect.yMax, 0),
        new Vector3(positionRect.xMax, positionRect.yMax, 0),
        ...
    };
    
    // 渲染到 RT
    commandBuffer.SetRenderTarget(scratchCard.RenderTarget);
    commandBuffer.DrawMesh(meshHole, ...);
    Graphics.ExecuteCommandBuffer(commandBuffer);
}

3. ScratchCardInput.cs(输入系统)

作用: 鼠标 / 触摸输入,支持新输入系统与旧输入系统。关键:

  • 不直接输出屏幕坐标
  • 输出纹理空间坐标(已经算好 UV 对应位置)

csharp

运行

private void Scratch()
{
    for (var i = 0; i < isScratching.Length; i++)
    {
        if (isScratching[i])
        {
            OnScratchHole?.Invoke(endInputData[i].Position, ...);
        }
    }
}

4. BaseData / ImageData / SpriteRendererData(UV 计算核心!)

这是你最关心的:坐标怎么转 UV?

核心算法:射线 + 重心坐标三角形映射

csharp

运行

// BaseData.cs
public virtual Vector2 GetScratchPosition(Vector2 position)
{
    var ray = Camera.ScreenPointToRay(position);
    if (plane.Raycast(ray, out enter))
    {
        var pointLocal = Surface.InverseTransformPoint(ray.GetPoint(enter));
        var uv = Triangle.GetUV(pointLocal); // 重心坐标算UV
        return Vector2.Scale(TextureSize, uv);
    }
}

Triangle.cs(重心坐标算法)

作用:无论形状是什么,都能算出精确 UV。这就是不规则 Sprite 也能精准刮擦的原因!

csharp

运行

public Vector2 GetUV(Vector3 point)
{
    var va = Vector3.Cross(v0 - v1, v0 - v2);
    var va1 = Vector3.Cross(distance1, distance2);
    ...
    var a1 = va1.magnitude / area * sign;
    var uv = uv0 * a1 + uv1 * a2 + uv2 * a3;
    return uv;
}

5. EraseProgress.cs(刮开进度计算)

作用: 读取 RenderTexture 像素,统计白色占比。

csharp

运行

private IEnumerator CalcProgress()
{
    var request = AsyncGPUReadback.RequestIntoNativeArray(...);
    yield return ...;
    
    for (var i = 0; i < pixelsBuffer.Length; i += bitsPerPixel)
    {
        progress += pixelsBuffer[i] / 255f;
    }
    progress /= 总像素数;
}

四、最重要问题:

修改 Sprite 形状(圆形 / 心形 / 不规则)还能正常涂抹

为什么能?

因为插件不依赖 Sprite 形状,不依赖碰撞体,不依赖矩形。它依赖的是:

1. UV 映射(Triangle 重心坐标)

ImageData / SpriteRendererData / MeshRendererData 都会计算精确 UV只在纹理有效区域绘制

2. Shader 会自动处理透明通道

plaintext

涂层显示 = 原始涂层颜色 * 遮罩RTAlpha * Sprite原图Alpha

3. 即使是透明镂空图,刮擦也不会溢出

插件在 BaseData 层就把坐标限制在 UV (0~1) 内。

五、非遗剪纸互动涂抹效果核心算法

本章节仅聚焦涂抹擦除算法本身,UI、动画、控制逻辑均简化说明,完全结合插件源码与不规则剪纸形状的实际交互场景。

5.1 剪纸涂抹需求说明

  • 上层:白色剪纸遮罩(圆形 / 八边形等不规则 Sprite)
  • 下层:红色剪纸底图
  • 交互逻辑:手指涂抹 → 擦除遮罩 → 露出剪纸 → 达到进度触发展开
  • 核心要求:仅在剪纸形状内响应涂抹,透明区域不生效

5.2 涂抹算法整体流程

涂抹效果由插件 5 个核心脚本协同实现,标准执行链路:

plaintext

触摸输入 → 坐标转换 → UV 精确映射 → 刮痕绘制到 RT → Shader 遮罩混合显示

5.3 阶段一:触摸输入采集(ScratchCardInput.cs)

插件兼容新旧输入系统,获取屏幕坐标并进行射线遮挡检测,保证涂抹只在有效区域触发。

csharp

运行

// 核心:捕获触摸/鼠标,输出纹理空间坐标
private void SetInputData(int fingerId, Vector2 position, float pressure = 1f)
{
    // 射线检测:过滤剪纸外部透明区域误触
    if (CheckCanvasRaycasts && raycastController.IsBlock(position))
        return;

    // 坐标转换:屏幕坐标 → 纹理坐标
    scratchPosition = OnScratch(position);

    // 触发涂抹逻辑
    OnScratchHole?.Invoke(endInputData[i].Position, pressure);
}

作用:精准过滤无效区域触摸,只响应剪纸范围内的涂抹操作。

5.4 阶段二:坐标到 UV 精确映射(BaseData + ImageData + Triangle)

这是不规则剪纸能精准涂抹的核心算法,也是整套方案的关键。

(1)坐标转换逻辑(BaseData.cs)

屏幕坐标 → 局部坐标 → 标准 UV 坐标

csharp

运行

public virtual Vector2 GetScratchPosition(Vector2 position)
{
    var ray = Camera.ScreenPointToRay(position);
    if (plane.Raycast(ray, out var enter))
    {
        // 世界坐标转局部坐标
        var pointLocal = Surface.InverseTransformPoint(ray.GetPoint(enter));
        // 重心坐标算法计算精确UV
        var uv = Triangle.GetUV(pointLocal);
        // 输出纹理像素位置
        return Vector2.Scale(TextureSize, uv);
    }
    return Vector2.zero;
}

(2)不规则形状 UV 计算(Triangle.cs)

采用三角形重心坐标算法,不依赖碰撞体、不限制矩形,支持任意形状 Sprite。

csharp

运行

public Vector2 GetUV(Vector3 point)
{
    // 计算重心权重
    var va = Vector3.Cross(v0 - v1, v0 - v2);
    var va1 = Vector3.Cross(distance1, distance2);
    var area = va.magnitude;

    var a1 = va1.magnitude / area * sign;
    var a2 = va2.magnitude / area * sign;
    var a3 = va3.magnitude / area * sign;

    // 输出最终UV
    return uv0 * a1 + uv1 * a2 + uv2 * a3;
}

结论:无论剪纸是圆形、八边形、心形或复杂镂空,UV 映射均准确,涂抹不溢出、不错位

5.5 阶段三:刮痕绘制到 RenderTexture(ScratchCardRenderer.cs)

插件使用 CommandBuffer 高性能渲染,将笔刷痕迹实时绘制到遮罩纹理(RT)。

csharp

运行

// 涂抹单个刮痕(核心方法)
public void ScratchHole(Vector2 position, float pressure = 1f)
{
    // 1. 计算笔刷在RT上的绘制区域
    var positionRect = new Rect(
        (position.x - brushSize * pressure) / textureWidth,
        (position.y - brushSize * pressure) / textureHeight,
        ...);

    // 2. 构建笔刷四边形Mesh
    meshHole.vertices = new[]
    {
        new Vector3(positionRect.xMin, positionRect.yMax, 0),
        new Vector3(positionRect.xMax, positionRect.yMax, 0),
        new Vector3(positionRect.xMax, positionRect.yMin, 0),
        new Vector3(positionRect.xMin, positionRect.yMin, 0)
    };

    // 3. 渲染到遮罩纹理RT
    commandBuffer.SetRenderTarget(scratchCard.RenderTarget);
    commandBuffer.DrawMesh(meshHole, Matrix4x4.identity, scratchCard.BrushMaterial);
    Graphics.ExecuteCommandBuffer(commandBuffer);
}

作用:RT 白色 = 已刮开,RT 黑色 = 未刮开,用遮罩记录所有涂抹痕迹。

5.6 阶段四:Shader 遮罩混合(最终显示效果)

插件内置 Shader 对两层纹理做透明度混合:

  • 上层:剪纸白色遮罩
  • 下层:RT 涂抹遮罩

plaintext

最终透明度 = 剪纸遮罩Alpha × RT遮罩Alpha
  • RT 白色 → 透明度为 0 → 露出下层剪纸
  • RT 黑色 → 透明度为 1 → 显示白色遮罩实现直观的 “涂抹即擦除” 视觉效果。

5.7 阶段五:涂抹进度计算(EraseProgress.cs)

通过 AsyncGPUReadback 异步读取 RT 像素,统计刮开区域占比,得到涂抹进度。

csharp

运行

private IEnumerator CalcProgress()
{
    // 异步读取RT像素数据
    var request = AsyncGPUReadback.RequestIntoNativeArray(ref pixelsBuffer, card.RenderTexture);
    yield return new WaitUntil(() => request.done);

    // 统计已刮开(白色)像素占比
    for (var i = 0; i < pixelsBuffer.Length; i += bitsPerPixel)
    {
        progress += pixelsBuffer[i] / 255f;
    }
    // 计算最终进度百分比
    progress /= 总像素数;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐