本文已参与「新人创作礼」活动,一起开启掘金创作之路。
布料缝合--(一)选取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上距离射线交点最近的投影点的坐标。