布料缝合--(一)选取Mesh缝合边界顶点

391 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

布料缝合--(一)选取Mesh缝合边界顶点

布料缝合--(二)绘制缝线 - 掘金 (juejin.cn)

布料缝合--(三)重置Mesh顶点、三角面 - 掘金 (juejin.cn)

想要完成一个缝合两块Mesh的功能,首先想到的就是借助ObiCloth插件,因为之前看到过改插件提供了缝合Mesh的API(参考Obi Stitcher)。 那么如何确定缝合区域?亦为本文的目的。

实现的思路是:

1)记录mesh全部顶点信息,找所有"边"顶点;

2)记录鼠标射线照射在mesh上的位置;

3)在所有"边"顶点中找到离射线交点最近的位置(投影点最近);

代码实现:

using System.Collections.Generic;
using UnityEngine;

public class AnchoredCursor : MonoBehaviour
{
    public CursorPoint cursorPoint;
    public bool isOnBoundary;
    private GameObject lastTarget;
    public List<EdgeHelpers.Edge> boundary;
    private const float error = 0.01f;

    private readonly struct Line
    {
        public readonly Vector3 pointStart;
        public readonly Vector3 pointEnd;

        public Line(Vector3 start, Vector3 end)
        {
            pointStart = start;
            pointEnd = end;
        }
    }

    //将鼠标吸附至鼠标所在三角形最近的一条边上
    /// <summary>
    /// 返回point在target上最近的一个边界上的投影
    /// </summary>
    /// <param name="target"></param>
    /// <param name="point"></param>
    /// <returns></returns>
    public Vector3 GetAnchorPosition(GameObject target, Vector3 point, ref bool added, List<EdgeHelpers.Edge> gather,
        bool isReverse)
    {
        MeshFilter targetMeshFilter = target.GetComponent<MeshFilter>();
        var mesh = targetMeshFilter.mesh;
        Vector3[] vertices = mesh.vertices;
        int[] triangles = mesh.triangles;

        var edge = getCloestEdge(target, point);
        Vector3 p1 = target.transform.TransformPoint(vertices[edge.v1]);
        Vector3 p2 = target.transform.TransformPoint(vertices[edge.v2]);
        Vector3 projectPOS = GetPorjection(point, p1, p2);

        if (gather != null && !gather.Contains(edge))
        {
            //第一个edge,直接插入
            if (gather.Count == 0)
            {
                gather.Add(edge);
                added = true;
            }
            //后续edge,保证是连续的
            else
            {
                //同步翻转
                if (isReverse)
                {
                    edge = new EdgeHelpers.Edge() {v1 = edge.v2, v2 = edge.v1};
                }

                //插入顶端、或末端,若不相邻则不插入
                if (edge.v2 == gather[0].v1)
                {
                    gather.Insert(0, edge);
                    added = true;
                }
                else if (edge.v1 == gather[gather.Count - 1].v2)
                {
                    gather.Add(edge);
                    added = true;
                }
            }
        }

        return projectPOS;
    }

    private void calcMeshSides(MeshFilter targetMesh)
    {
        var _boundary = EdgeHelpers.GetEdges(targetMesh.mesh.triangles).FindBoundary();
        this.boundary = _boundary;
    }

    private EdgeHelpers.Edge getCloestEdge(GameObject target, Vector3 pos)
    {
        MeshFilter targetMesh = target.GetComponent<MeshFilter>();
        Vector3[] vertices = targetMesh.mesh.vertices;
        if (lastTarget == null || lastTarget != target)
            calcMeshSides(targetMesh);
        lastTarget = target;

        float minDis = -1f;
        int index = -1;

        // foreach (var edge in boundary)
        for (int i = 0; i < boundary.Count; i++)
        {
            var edge = boundary[i];
            Vector3 p1 = target.transform.TransformPoint(vertices[edge.v1]);
            Vector3 p2 = target.transform.TransformPoint(vertices[edge.v2]);
            Vector3 projectPOS = GetPorjection(pos, p1, p2);

            if (index == -1 || minDis > Vector3.Distance(pos, projectPOS))
            {
                index = i;
                minDis = Vector3.Distance(pos, projectPOS);
            }
        }

        if (index == -1) return boundary[0];
        return boundary[index];
    }

    private float distancePoint2Line(Vector3 point, Line line)
    {
        Vector3 linePoint1 = line.pointStart;
        Vector3 linePoint2 = line.pointEnd;
        float fProj = Vector3.Dot(point - linePoint1, (linePoint1 - linePoint2).normalized);
        return Mathf.Sqrt((point - linePoint1).sqrMagnitude - fProj * fProj);
    }

    static Vector3 GetPorjection(Vector3 c, Vector3 a, Vector3 b) // a和b是线段的两个端点, c是检测点
    {
        Vector3 ab = b - a;

        Vector3 ac = c - a;

        float f = Vector3.Dot(ab, ac);

        if (f < 0) return a;

        float d = Vector3.Dot(ab, ab);

        if (f > d) return b;

        f = f / d;

        Vector3 D = a + f * ab; // c在ab线段上的投影点

        return D;
    }


    bool onSegment(Vector3 Q, Vector3 Pi, Vector3 Pj)
    {
        var flag = Mathf.Abs((Q.x - Pi.x) * (Pj.y - Pi.y) - (Pj.x - Pi.x) * (Q.y - Pi.y)) <= error;
        return flag
               && Mathf.Min(Pi.x, Pj.x) <= (Q.x + error) && (Q.x - error) <= Mathf.Max(Pi.x, Pj.x)
               && Mathf.Min(Pi.y, Pj.y) <= (Q.y + error) && (Q.y - error) <= Mathf.Max(Pi.y, Pj.y);
    }


    public bool SetCursorToAnchorPosition(GameObject target, Vector3 pos, List<EdgeHelpers.Edge> gather = null,
        bool isReverse = false)
    {
        var added = false;
        this.isOnBoundary = true;
        var anchorPos = GetAnchorPosition(target, pos, ref added, gather, isReverse);
        cursorPoint.SetPos(anchorPos);
        return added;
    }

    public void SetCursorToPosition(Vector3 pos)
    {
        this.isOnBoundary = false;
        cursorPoint.SetPos(pos);
    }

    public void SetCursorActiveTo(bool statu)
    {
        cursorPoint.ShowHide(statu);
    }

    public bool IsOnBoundary()
    {
        return isOnBoundary;
    }
}

public static class EdgeHelpers
{
    public struct Edge
    {
        public int v1;
        public int v2;
        public int triangleIndex;

        public Edge(int aV1, int aV2, int aIndex)
        {
            v1 = aV1;
            v2 = aV2;
            triangleIndex = aIndex;
        }
    }

    public static List<Edge> GetEdges(int[] aIndices)
    {
        List<Edge> result = new List<Edge>();
        for (int i = 0; i < aIndices.Length; i += 3)
        {
            int v1 = aIndices[i];
            int v2 = aIndices[i + 1];
            int v3 = aIndices[i + 2];
            result.Add(new Edge(v1, v2, i));
            result.Add(new Edge(v2, v3, i));
            result.Add(new Edge(v3, v1, i));
        }

        return result;
    }

    public static List<Edge> FindBoundary(this List<Edge> aEdges)
    {
        List<Edge> result = new List<Edge>(aEdges);
        for (int i = result.Count - 1; i > 0; i--)
        {
            for (int n = i - 1; n >= 0; n--)
            {
                if (result[i].v1 == result[n].v2 && result[i].v2 == result[n].v1)
                {
                    // shared edge so remove both
                    result.RemoveAt(i);
                    result.RemoveAt(n);
                    i--;
                    break;
                }
            }
        }

        return result;
    }

    public static List<Edge> SortEdges(this List<Edge> aEdges)
    {
        List<Edge> result = new List<Edge>(aEdges);
        for (int i = 0; i < result.Count - 2; i++)
        {
            Edge E = result[i];
            for (int n = i + 1; n < result.Count; n++)
            {
                Edge a = result[n];
                if (E.v2 == a.v1)
                {
                    // in this case they are already in order so just continoue with the next one
                    if (n == i + 1)
                        break;
                    // if we found a match, swap them with the next one after "i"
                    result[n] = result[i + 1];
                    result[i + 1] = a;
                    break;
                }
            }
        }

        return result;
    }
}

其中GetAnchorPosition方法需要传入被检测的Mesh对象,射线交点位置Point,该方法会返回mesh上距离射线交点最近的投影点的坐标。