You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何检测Sprite边界内的涂色接近完成状态?

在Unity中检测Sprite边界内的涂色完成度

针对你的涂色游戏场景,这里提供几种实用方案来检测Sprite边界内的涂色是否接近完成:

方案1:基于纹理像素统计(最直接易用)

核心思路是统计Sprite有效区域内,已涂色像素占总需涂色像素的比例,当比例达到设定阈值时判定接近完成。

实现步骤

  1. 准备可读写纹理:Unity中Sprite原纹理默认不可写,需创建可读写副本用于像素检测。
  2. 统计总需涂色像素:遍历Sprite的纹理矩形区域,只统计不透明区域内的初始底色像素(比如立方体Sprite未涂色时的颜色)。
  3. 实时统计已涂色像素:每次涂色操作后,再次遍历Sprite区域,统计非底色的像素数量。
  4. 计算完成度:用已涂色像素数除以总需涂色像素数,与阈值(如95%)对比判断。

代码示例

using UnityEngine;

public class PaintCompletionChecker : MonoBehaviour
{
    [SerializeField] private SpriteRenderer targetSpriteRenderer;
    [SerializeField] private Color baseUnpaintedColor = Color.white;
    [SerializeField] private float completionThreshold = 0.95f;
    private int totalTargetPixels;
    private Texture2D editablePaintTexture;

    private void Start()
    {
        Sprite targetSprite = targetSpriteRenderer.sprite;
        Texture2D originalTex = targetSprite.texture;

        // 创建可读写的纹理副本
        editablePaintTexture = new Texture2D(originalTex.width, originalTex.height, originalTex.format, false);
        Graphics.CopyTexture(originalTex, editablePaintTexture);
        editablePaintTexture.filterMode = FilterMode.Point;

        // 统计Sprite不透明区域内的总需涂色像素
        Rect spriteTexRect = targetSprite.textureRect;
        for (int x = (int)spriteTexRect.x; x < spriteTexRect.x + spriteTexRect.width; x++)
        {
            for (int y = (int)spriteTexRect.y; y < spriteTexRect.y + spriteTexRect.height; y++)
            {
                Color pixel = editablePaintTexture.GetPixel(x, y);
                // 只统计不透明的底色像素
                if (pixel.a > 0.1f && ColorUtility.AreColorsEqual(pixel, baseUnpaintedColor, 0.01f))
                {
                    totalTargetPixels++;
                }
            }
        }
    }

    // 调用此方法判断是否接近完成
    public bool IsNearCompletion()
    {
        if (totalTargetPixels == 0) return false;

        Rect spriteTexRect = targetSpriteRenderer.sprite.textureRect;
        int paintedPixels = 0;

        for (int x = (int)spriteTexRect.x; x < spriteTexRect.x + spriteTexRect.width; x++)
        {
            for (int y = (int)spriteTexRect.y; y < spriteTexRect.y + spriteTexRect.height; y++)
            {
                Color pixel = editablePaintTexture.GetPixel(x, y);
                if (pixel.a > 0.1f && !ColorUtility.AreColorsEqual(pixel, baseUnpaintedColor, 0.01f))
                {
                    paintedPixels++;
                }
            }
        }

        float completionRate = (float)paintedPixels / totalTargetPixels;
        return completionRate >= completionThreshold;
    }

    // 与你的涂色逻辑结合,每次涂色后更新纹理副本
    public void SyncPaintTexture(Texture2D updatedTexture)
    {
        Graphics.CopyTexture(updatedTexture, editablePaintTexture);
    }
}

注意事项

  • 确保纹理的Read/Write Enabled选项在Import Settings中开启,或通过代码创建副本(如上示例)。
  • 如果涂色支持多种颜色,只需修改判断条件为“非底色即可”。
  • 大纹理下遍历像素会卡顿,可以分多帧完成统计(比如每帧只遍历一部分行)。

方案2:基于区域掩码的碰撞记录(适合交互驱动的涂色)

如果你的指针涂色是通过碰撞交互实现的,可以用掩码数组记录已涂色的区域,避免重复统计像素。

实现步骤

  1. 创建Sprite碰撞轮廓:给SpriteRenderer添加PolygonCollider2D,并使用Edit Collider工具匹配Sprite的边界。
  2. 初始化涂色掩码:创建布尔数组,对应Sprite有效区域的每个像素,初始标记为未涂色。
  3. 记录涂色区域:当指针接触立方体时,将世界坐标转换为纹理坐标,标记掩码数组中对应位置为已涂色。
  4. 计算完成度:统计掩码中已标记的数量占总需涂色像素的比例。

代码示例

using UnityEngine;

public class PaintMaskCompletion : MonoBehaviour
{
    [SerializeField] private SpriteRenderer targetSpriteRenderer;
    [SerializeField] private float completionThreshold = 0.95f;
    private bool[,] paintedMask;
    private int totalTargetPixels;
    private int paintedPixelCount;

    private void Start()
    {
        Sprite targetSprite = targetSpriteRenderer.sprite;
        Rect spriteTexRect = targetSprite.textureRect;
        int rectWidth = (int)spriteTexRect.width;
        int rectHeight = (int)spriteTexRect.height;

        paintedMask = new bool[rectWidth, rectHeight];
        totalTargetPixels = 0;

        // 初始化掩码和总需涂色像素数
        for (int x = 0; x < rectWidth; x++)
        {
            for (int y = 0; y < rectHeight; y++)
            {
                Color pixel = targetSprite.texture.GetPixel((int)spriteTexRect.x + x, (int)spriteTexRect.y + y);
                if (pixel.a > 0.1f)
                {
                    totalTargetPixels++;
                    paintedMask[x, y] = false;
                }
                else
                {
                    paintedMask[x, y] = true; // 透明区域跳过
                }
            }
        }
    }

    // 指针涂色时调用,传入世界坐标的接触点
    public void MarkPaintedArea(Vector3 worldContactPoint)
    {
        Sprite targetSprite = targetSpriteRenderer.sprite;
        Rect spriteTexRect = targetSprite.textureRect;

        // 转换为Sprite本地坐标
        Vector2 localPos = targetSpriteRenderer.WorldToLocalPoint(worldContactPoint);
        // 转换为纹理坐标(注意Sprite的Pivot影响,需调整)
        Vector2 texturePos = new Vector2(
            spriteTexRect.x + (localPos.x + targetSprite.bounds.size.x / 2) * targetSprite.pixelsPerUnit,
            spriteTexRect.y + (localPos.y + targetSprite.bounds.size.y / 2) * targetSprite.pixelsPerUnit
        );

        int x = (int)texturePos.x;
        int y = (int)texturePos.y;

        // 检查坐标是否在掩码范围内
        if (x >= 0 && x < paintedMask.GetLength(0) && y >= 0 && y < paintedMask.GetLength(1))
        {
            if (!paintedMask[x, y])
            {
                paintedMask[x, y] = true;
                paintedPixelCount++;
            }
        }
    }

    public bool IsNearCompletion()
    {
        if (totalTargetPixels == 0) return false;
        float completionRate = (float)paintedPixelCount / totalTargetPixels;
        return completionRate >= completionThreshold;
    }
}

注意事项

  • 坐标转换时需注意Sprite的Pivot设置,避免偏移错误。
  • 如果指针是管状(有宽度),可以标记一个区域的像素,而不是单个点。

方案3:GPU计算优化(适合大纹理场景)

如果你的Sprite纹理尺寸较大,CPU遍历像素会导致帧率下降,可以用Compute Shader在GPU端完成像素统计。

实现思路

  1. 编写Compute Shader,定义用于存储计数的结构化缓冲区。
  2. 遍历Sprite的纹理区域,统计符合条件的像素数量,将结果累加到缓冲区。
  3. 在C#代码中读取计算结果,判断完成度。

关键代码片段(Compute Shader)

#pragma kernel CountPaintedPixels

Texture2D inputTexture;
RWStructuredBuffer<int> pixelCountBuffer;
float4 baseColor;
int spriteX;
int spriteY;
int spriteWidth;
int spriteHeight;

[numthreads(8,8,1)]
void CountPaintedPixels (uint3 id : SV_DispatchThreadID)
{
    if (id.x < spriteWidth && id.y < spriteHeight)
    {
        int texX = spriteX + id.x;
        int texY = spriteY + id.y;
        float4 pixel = inputTexture[uint2(texX, texY)];
        
        // 判断是否为已涂色像素(非底色且不透明)
        if (pixel.a > 0.1f && distance(pixel, baseColor) > 0.01f)
        {
            InterlockedAdd(pixelCountBuffer[0], 1);
        }
    }
}

C#端调用逻辑

// 初始化Compute Shader和缓冲区
ComputeShader countShader;
ComputeBuffer pixelCountBuffer;

private void Start()
{
    pixelCountBuffer = new ComputeBuffer(1, sizeof(int));
    int[] initCount = {0};
    pixelCountBuffer.SetData(initCount);
}

public int GetPaintedPixelCount(Texture2D tex, Rect spriteRect)
{
    countShader.SetTexture(0, "inputTexture", tex);
    countShader.SetBuffer(0, "pixelCountBuffer", pixelCountBuffer);
    countShader.SetVector("baseColor", baseUnpaintedColor);
    countShader.SetInt("spriteX", (int)spriteRect.x);
    countShader.SetInt("spriteY", (int)spriteRect.y);
    countShader.SetInt("spriteWidth", (int)spriteRect.width);
    countShader.SetInt("spriteHeight", (int)spriteRect.height);

    // 调度线程组
    int threadGroupsX = Mathf.CeilToInt(spriteRect.width / 8f);
    int threadGroupsY = Mathf.CeilToInt(spriteRect.height / 8f);
    countShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);

    // 读取结果
    int[] result = new int[1];
    pixelCountBuffer.GetData(result);
    return result[0];
}

注意事项

  • 需要熟悉Compute Shader的基础语法和线程组调度规则。
  • 确保纹理在GPU端可访问,设置正确的纹理格式。

内容的提问来源于stack exchange,提问作者Axay Gadhia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 07:55:29