You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity中基于顶点与UV渲染精灵三角形时仅第一个三角形成功渲染的问题排查

Unity中基于顶点与UV渲染精灵三角形时仅第一个三角形成功渲染的问题排查

看起来你遇到的核心问题是重心权重(面积法)的计算公式完全写错了,导致第二个三角形的所有像素都被误判为“在三角形外”(权重为负),最终被continue跳过,没有任何像素被渲染。我们一步步拆解问题并修复:

问题根源:错误的权重计算逻辑

你的代码里,分母denom和权重weightA/weightB的推导完全偏离了三角形面积的数学逻辑。第一个三角形碰巧因为顶点顺序的原因,权重计算结果为正,所以能正常渲染;但第二个三角形的顶点顺序(顺时针)会导致权重全为负,直接被过滤。

看你代码里的错误核心片段:

var cX_bX = vertC.x - vertB.x;
var bY_cY = vertB.y - vertC.y;
var aX_cX = vertA.x - vertC.x;
var denom = (bY_cY * aX_cX) + (cX_bX * aX_cX); // 完全错误的分母计算,和三角形面积无关
// ...
var weightA = ((bY_cY * x_cX) + (cX_bX * y_cY)) / denom; // 基于错误分母的权重自然也不对

修复方案:正确的重心坐标(面积法)实现

我们用向量叉乘来计算三角形的有向面积,这是重心权重计算的标准数学依据。以下是修正后的完整代码,我会保留你的核心遍历逻辑,只替换错误的权重计算部分:

private void RenderTight(Texture2D targetTexture, Sprite sprite)
{
    if (!sprite.texture.isReadable)
    {
        Debug.LogError("Sprite纹理必须设置为可读状态!");
        return;
    }

    // 遍历每个三角形(每3个索引为一组)
    for (int i = 0; i < sprite.triangles.Length; i += 3)
    {
        int idxA = sprite.triangles[i];
        int idxB = sprite.triangles[i + 1];
        int idxC = sprite.triangles[i + 2];

        // 将Sprite局部顶点转换为像素坐标
        Vector2 vertA = sprite.vertices[idxA] * sprite.pixelsPerUnit;
        Vector2 vertB = sprite.vertices[idxB] * sprite.pixelsPerUnit;
        Vector2 vertC = sprite.vertices[idxC] * sprite.pixelsPerUnit;

        // 计算三角形的包围盒,缩小遍历范围
        int minX = Mathf.FloorToInt(Mathf.Min(vertA.x, vertB.x, vertC.x));
        int maxX = Mathf.CeilToInt(Mathf.Max(vertA.x, vertB.x, vertC.x));
        int minY = Mathf.FloorToInt(Mathf.Min(vertA.y, vertB.y, vertC.y));
        int maxY = Mathf.CeilToInt(Mathf.Max(vertA.y, vertB.y, vertC.y));

        // 计算三角形ABC的有向面积的2倍(叉乘结果)
        Vector2 vAB = vertB - vertC;
        Vector2 vAC = vertA - vertC;
        float triangleArea2x = vAB.x * vAC.y - vAB.y * vAC.x;

        // 三点共线,直接跳过这个无效三角形
        if (Mathf.Abs(triangleArea2x) < 0.001f)
            continue;

        // 遍历包围盒内的每个像素
        for (int x = minX; x <= maxX; x++)
        {
            for (int y = minY; y <= maxY; y++)
            {
                Vector2 pixelPos = new Vector2(x, y);

                // 计算顶点A的权重:子三角形PBC的面积 / 原三角形ABC的面积
                Vector2 vPB = vertB - pixelPos;
                Vector2 vPC = vertC - pixelPos;
                float areaPBC2x = vPB.x * vPC.y - vPB.y * vPC.x;
                float weightA = areaPBC2x / triangleArea2x;

                // 计算顶点B的权重:子三角形APC的面积 / 原三角形ABC的面积
                Vector2 vPA = vertA - pixelPos;
                Vector2 vPC2 = vertC - pixelPos;
                float areaAPC2x = vPA.x * vPC2.y - vPA.y * vPC2.x;
                float weightB = areaAPC2x / triangleArea2x;

                // 顶点C的权重由权重和为1推导
                float weightC = 1 - weightA - weightB;

                // 判断像素是否在三角形内:允许微小负权重(容忍浮点计算误差)
                if (weightA < -0.001f || weightB < -0.001f || weightC < -0.001f)
                    continue;

                // 权重归一化,解决浮点误差导致的权重和略偏离1的问题
                float totalWeight = weightA + weightB + weightC;
                weightA /= totalWeight;
                weightB /= totalWeight;
                weightC /= totalWeight;

                // 计算最终采样UV
                Vector2 uv = sprite.uv[idxA] * weightA + sprite.uv[idxB] * weightB + sprite.uv[idxC] * weightC;

                // 注意:Unity的Texture2D.GetPixel的Y轴从纹理顶部开始,而Sprite的UV Y轴从底部开始,需要转换
                int texX = Mathf.Clamp(Mathf.RoundToInt(uv.x * sprite.texture.width), 0, sprite.texture.width - 1);
                int texY = Mathf.Clamp(Mathf.RoundToInt((1 - uv.y) * sprite.texture.height), 0, sprite.texture.height - 1);
                Color sampledColor = sprite.texture.GetPixel(texX, texY);

                // 将像素写入目标纹理(确保坐标在目标纹理范围内)
                if (x >= 0 && x < targetTexture.width && y >= 0 && y < targetTexture.height)
                {
                    targetTexture.SetPixel(x, y, sampledColor);
                }
            }
        }
    }

    // 应用纹理修改,否则不会生效
    targetTexture.Apply();
}

关键修复细节说明

  1. 正确的面积计算:用向量叉乘vAB.x * vAC.y - vAB.y * vAC.x计算三角形的有向面积,这是权重计算的核心,兼容顺时针/逆时针顶点顺序。
  2. 误差容忍:判断像素是否在三角形内时,用-0.001f代替0,避免浮点计算误差导致边缘像素被误判。
  3. UV坐标转换:添加(1 - uv.y)的转换,解决Unity纹理GetPixel的Y轴方向与Sprite UV Y轴方向相反的问题。
  4. 权重归一化:处理浮点误差导致的权重和略偏离1的问题,避免UV采样出现偏移。

额外注意事项

  • 目标纹理初始化:你需要提前创建大小足够的目标纹理(可通过sprite.bounds.size * sprite.pixelsPerUnit计算所需宽高),并设置为可读可写。
  • 性能优化:这种逐像素遍历的方法性能很低,仅适合小Sprite。如果是大Sprite,建议改用Unity的Graphics.DrawMesh渲染到RenderTexture,效率会提升几个数量级。
  • Sprite顶点顺序:Unity的Sprite三角形默认是顺时针顺序,修正后的代码已经兼容顺时针/逆时针两种顶点顺序,无需额外调整。

现在测试修正后的代码,第二个三角形应该能正常渲染了。如果还有问题,可以检查目标纹理的大小是否足够,或者Sprite的UV设置是否符合预期。

火山引擎 最新活动