如何在 Unity 中只找到网格的 "top"
How to find only the "top" of a mesh in Unity
我需要在 Unity 中确定构成其 "top" 的多面体(网格)的顶点;即沿 y 轴最远的点。
这样做的原因是我正在创建一个程序环境,其中有许多活跃的网格动态;其中之一将海洋塑造成波形。做工很漂亮,但也扭曲了水底,不太理想。
我觉得答案可能涉及某种投影,但我不确定如何。
免责声明:我以前没有以这种方式处理过网格,但我认为这里有 2 种思考方式可能会让您进一步走向完全可行的解决方案。
可能的解决方案:
使用 Mesh.Bounds 属性.
虽然我怀疑这只适用于平面。
来源:https://docs.unity3d.com/ScriptReference/Mesh-bounds.html
使用 Mesh.Vertices 并根据您使用的轴作为顶部(y 轴),使用每个数组索引中的其他 2 个轴(x 和 z)来计算该特定位置的最高点网格的一部分。把这些和他全部收集起来,你就有了表面。
Mesh.Vertices returns 一个 Vector3 数组。
假设您有这些坐标。
Vector3 (443, 31, 543);
Vector3 (443, 32, 543);
Vector3 (443, 33, 543);
Vector3 (443, 34, 543);
Vector3 (443, 35, 543);
此处的最高坐标是 y 为 35 的坐标,假设正 y 轴是您认为的顶部。
根据给定的顶点编号,可能需要将某些顶点组合在一起,以便 Vector3 (443, 35, 543);
和 Vector3 (443.1, 35, 543.2);
处于同一比较中。
来源:https://docs.unity3d.com/ScriptReference/Mesh-vertices.html
希望对您有所帮助。
阅读前请注意:这是我最初的方法,在实践中效果很好,但后来发现有一个更简单、更易读的解决方案发布在网上。
首先,让我强调一下,我仍在单元测试这段代码,它可能有错误。到目前为止,一切都很好。
这是我所做的。
我把它写在白板上并注意到我正在垂直向下投影 y。因此,我需要做的第一件事就是得到每个三角形的平均值 y-value。
(请注意,这意味着这在 self-intersecting 网格的情况下将毫无用处;要处理这些问题,您必须为 "top"其实就是。)
之后,我根据平均 y 值对三角形进行排序,最高的 y-value 排在列表的前面。
之后就是排序问题了!对于每个三角形,如果它与我之前保留的 none 个三角形相交,那么我将它添加到 top-side 个三角形的列表中。我为此使用了 point-in-triangle test。如果确实相交,那么它就在我的 top-side 三角形之一下面,可以跳过。
另一种不起作用的情况是三角形与其他三角形重叠但不相交的实例;这意味着边相互交叉的任何情况,但 none 个顶点是 "inside" 另一个三角形的边界。有点像 Star-of-David 的形状;此代码不会将它们检测为穿越。
正如我所说,我仍在对可预见的情况进行单元测试;但我的代码如下。我采用了函数式范式,因为它使事情易于理解,并且适用于现代游戏软件。请注意,对于任何类型的网格动态,您都需要记住这一点;您不希望每次要更改网格时都扫描每个三角形!
/// <summary>
/// Gets the average y-height of a triangle of Vector3s.
/// </summary>
static Func<Vector3, Vector3, Vector3, float> yHeight = (a, b, c) => (a.y + b.y + c.y)/3f;
/// <summary>
/// Projects a provided triangle onto the X-Z plane. (If you need to project onto any other plane, this is the function to alter or
/// replace.)
/// </summary>
static Func<Vector3[], Vector3[]> ProjectToY = (points) =>
new Vector3[]{
new Vector3(points[0].x, 0, points[0].z),
new Vector3(points[1].x, 0, points[1].z),
new Vector3(points[2].x, 0, points[2].z)
};
/// <summary>
/// Determines if point p is on the same side of any given edge, as the opposite edge.
/// </summary>
static Func<Vector3, Vector3, Vector3, Vector3, bool> SameSide = (p1, p2, a, b) => {
return Vector3.Dot(
Vector3.Cross (b - a, p1 - a),
Vector3.Cross (b - a, p2 - a)
) >= 0;
};
/// <summary>
/// Determines whether a point is inside the triangle formed by given Vector3s
/// </summary>
static Func<Vector3, Vector3, Vector3, Vector3, bool> PointInTriangle = (p, a, b, c) => {
return SameSide(p, a, b, c) && SameSide(p, b, a, c) && SameSide(p, c, a, b);
};
/// <summary>
/// Determines whether triangle of Vector3s one intersects triangle of Vector3s two. (Note: Does not check the other way around.
/// Presumes both are on the same plane.)
/// </summary>
static Func<Vector3[], Vector3[], bool> TriangleInTriangle = (one, two) =>
PointInTriangle(one[0], two[0], two[1], two[2]) ||
PointInTriangle(one[1], two[0], two[1], two[2]) ||
PointInTriangle(one[2], two[0], two[1], two[2])
;
/// <summary>
/// Determines whether either of two triangles intersect the other. The one exception would be a case where the triangles overlap,
/// but do not intersect one another's points. This would require segment intersection tests.
/// </summary>
static Func<Vector3[], Vector3[], bool> TrianglesIntersect = (one, two) =>
TriangleInTriangle(one, two) || TriangleInTriangle(two, one)
;
/// <summary>
/// Organizes a dictionary of triangle indices to average distances by distance, with the furthest at the front.
/// </summary>
static Func<Vector3[], int[], Dictionary<int, float>> OrderByHeight = (vertices, triangles) => {
Dictionary<int, float> height = new Dictionary<int, float>();
for(int i = 0; i < triangles.Length; i += 3)
height.Add(i / 3, yHeight(vertices[triangles[i]], vertices[triangles[i+1]], vertices[triangles[i + 2]]));
return height;
};
/// <summary>
/// Sorts a dictionary of triangle indices to average heights, to a list of the same key value pairs, with the pairs of highest height at the front.
/// </summary>
static Func<Dictionary<int, float>, List<KeyValuePair<int, float>>> SortByHeight = (height) => {
List<KeyValuePair<int, float>> orderedHeight = height.ToList();
orderedHeight.Sort(
delegate(KeyValuePair<int, float> a, KeyValuePair<int, float> b) {
return a.Value > b.Value ? -1 : 1;
}
);
return orderedHeight;
};
/// <summary>
/// Determines whether a provided triangle, in KeyValuePair form of triangle to average distance, is in front of all triangles in a list of pairs.
/// </summary>
static Func<KeyValuePair<int, float>, List<KeyValuePair<int, float>>, Vector3[], bool> IsInFront = (pair, top, vertices) => {
foreach (KeyValuePair<int, float> prevPair in top) {
if (TrianglesIntersect (
ProjectToY (
new Vector3[]{ vertices [pair.Key * 3 + 0], vertices [pair.Key * 3 + 1], vertices [pair.Key * 3 + 2] }
),
ProjectToY (
new Vector3[] {
vertices [prevPair.Key * 3 + 0],
vertices [prevPair.Key * 3 + 1],
vertices [prevPair.Key * 3 + 2]
}
))) {
return false;
}
}
return true;
};
/// <summary>
/// Given a list of triangle indices to average heights, sorted by height with highest first, finds the triangles which are unoccluded by closer triangles.
/// </summary>
static Func<List<KeyValuePair<int, float>>, Vector3[], List<KeyValuePair<int, float>>> GetUnoccludedTriangles = (orderedHeight, vertices) => {
List<KeyValuePair<int, float>> top = new List<KeyValuePair<int, float>> ();
foreach (KeyValuePair<int, float> pair in orderedHeight) {
if(IsInFront(pair, top, vertices))
top.Add(pair);
}
return top;
};
/// <summary>
/// Converts indices of triangles to indices of the first vertex in the triangle.
/// </summary>
static Func<List<KeyValuePair<int, float>>, int[], int[]> ConvertTriangleIndicesToVertexIndices = (top, triangles) => {
int[] topArray = new int[top.Count * 3];
for(int i = 0; i < top.Count; i++) {
topArray[i * 3 + 0] = triangles[top[i].Key * 3 + 0];
topArray[i * 3 + 1] = triangles[top[i].Key * 3 + 1];
topArray[i * 3 + 2] = triangles[top[i].Key * 3 + 2];
}
return topArray;
};
//This seems to work well; but could work better. See to testing it thoroughly, and making it as functional, bulletproof, and short as possible.
/// <summary>
/// Determines the unoccluded "top" of a mesh of triangles, and returns it.
/// </summary>
static Func<Vector3[], int[], int[]> FindTop = (vertices, triangles) => {
return ConvertTriangleIndicesToVertexIndices(
GetUnoccludedTriangles(
SortByHeight(
OrderByHeight(vertices, triangles)
),
vertices),
triangles);
};
您需要使用来自网格的原始数据调用 FindTop 函数来查找 top-side 表面。这个算法有十几种不同的方法可以在其他情况下被扭曲,所以我试着很好地评论它。希望它能在将来帮助其他人。
当我绝对可靠地确信这在所有合理的情况下都有效时,我会接受这个作为答案;但任何有一点建议的人的进一步意见总是受欢迎的。
这就是我最终的结果。我退后一步,考虑我正在尝试做的事情的性质,即只改变顶面;以及有效的约束,这是我需要做的,而且很多都是在 CPU 方面。我还认为我之前的回答中的很多 material 都是多余的,因为我在程序构建网格时已经完成了等效操作。
法线始终朝向多边形可见的方向;所以他们是我解决问题的关键。我在单个 LINQ 查询中完成了它,可在此处获得。
public override void GetAppropriateIndices (Mesh mesh)
{
this.indices = new HashSet<int> (mesh.triangles.AsEnumerable ().Distinct ().Where (index =>
mesh.normals [index] == Vector3.up
).ToList());
}
或者,更灵活地,
public override void GetAppropriateIndices (Mesh mesh)
{
this.indices = new HashSet<int> (mesh.triangles.AsEnumerable ().Distinct ().Where (index =>
Vector3.Angle(mesh.normals [index], Vector3.up) < ε
).ToList());
}
其中 ε 是您愿意在角度上容忍的最大回旋余地。从技术上讲,第二个解决方案更好,因为它考虑了浮点错误以及并非每个法线都将精确地与我们所说的一样的事实,在任何语言中,API,或范例。
所有测试都已通过。我有一个程序生成的海洋切片,动画波浪 运行 仅穿过它的表面(减去用于测试目的的战略切口。)我的解决方案在工作中的图片:
从下面看,未应用网格动态的地方:
你可以在这里看到,由于表面下方的网格法线不够接近 Vector3.up,它没有被修改并保持矩形。
最后,当我将 ε 调到 90f 以上时会发生什么:
我不认为这绝对适用于所有情况,但它们确实有效地完成了工作(所有功能现在都只是一个 LINQ 行......怎么样?)和更具可读性。
在我忘记之前,纹理归功于 Textures.com。
我需要在 Unity 中确定构成其 "top" 的多面体(网格)的顶点;即沿 y 轴最远的点。
这样做的原因是我正在创建一个程序环境,其中有许多活跃的网格动态;其中之一将海洋塑造成波形。做工很漂亮,但也扭曲了水底,不太理想。
我觉得答案可能涉及某种投影,但我不确定如何。
免责声明:我以前没有以这种方式处理过网格,但我认为这里有 2 种思考方式可能会让您进一步走向完全可行的解决方案。
可能的解决方案:
使用 Mesh.Bounds 属性.
虽然我怀疑这只适用于平面。
来源:https://docs.unity3d.com/ScriptReference/Mesh-bounds.html
使用 Mesh.Vertices 并根据您使用的轴作为顶部(y 轴),使用每个数组索引中的其他 2 个轴(x 和 z)来计算该特定位置的最高点网格的一部分。把这些和他全部收集起来,你就有了表面。
Mesh.Vertices returns 一个 Vector3 数组。
假设您有这些坐标。
Vector3 (443, 31, 543);
Vector3 (443, 32, 543);
Vector3 (443, 33, 543);
Vector3 (443, 34, 543);
Vector3 (443, 35, 543);
此处的最高坐标是 y 为 35 的坐标,假设正 y 轴是您认为的顶部。
根据给定的顶点编号,可能需要将某些顶点组合在一起,以便 Vector3 (443, 35, 543);
和 Vector3 (443.1, 35, 543.2);
处于同一比较中。
来源:https://docs.unity3d.com/ScriptReference/Mesh-vertices.html
希望对您有所帮助。
阅读前请注意:这是我最初的方法,在实践中效果很好,但后来发现有一个更简单、更易读的解决方案发布在网上。
首先,让我强调一下,我仍在单元测试这段代码,它可能有错误。到目前为止,一切都很好。
这是我所做的。
我把它写在白板上并注意到我正在垂直向下投影 y。因此,我需要做的第一件事就是得到每个三角形的平均值 y-value。
(请注意,这意味着这在 self-intersecting 网格的情况下将毫无用处;要处理这些问题,您必须为 "top"其实就是。)
之后,我根据平均 y 值对三角形进行排序,最高的 y-value 排在列表的前面。
之后就是排序问题了!对于每个三角形,如果它与我之前保留的 none 个三角形相交,那么我将它添加到 top-side 个三角形的列表中。我为此使用了 point-in-triangle test。如果确实相交,那么它就在我的 top-side 三角形之一下面,可以跳过。
另一种不起作用的情况是三角形与其他三角形重叠但不相交的实例;这意味着边相互交叉的任何情况,但 none 个顶点是 "inside" 另一个三角形的边界。有点像 Star-of-David 的形状;此代码不会将它们检测为穿越。
正如我所说,我仍在对可预见的情况进行单元测试;但我的代码如下。我采用了函数式范式,因为它使事情易于理解,并且适用于现代游戏软件。请注意,对于任何类型的网格动态,您都需要记住这一点;您不希望每次要更改网格时都扫描每个三角形!
/// <summary>
/// Gets the average y-height of a triangle of Vector3s.
/// </summary>
static Func<Vector3, Vector3, Vector3, float> yHeight = (a, b, c) => (a.y + b.y + c.y)/3f;
/// <summary>
/// Projects a provided triangle onto the X-Z plane. (If you need to project onto any other plane, this is the function to alter or
/// replace.)
/// </summary>
static Func<Vector3[], Vector3[]> ProjectToY = (points) =>
new Vector3[]{
new Vector3(points[0].x, 0, points[0].z),
new Vector3(points[1].x, 0, points[1].z),
new Vector3(points[2].x, 0, points[2].z)
};
/// <summary>
/// Determines if point p is on the same side of any given edge, as the opposite edge.
/// </summary>
static Func<Vector3, Vector3, Vector3, Vector3, bool> SameSide = (p1, p2, a, b) => {
return Vector3.Dot(
Vector3.Cross (b - a, p1 - a),
Vector3.Cross (b - a, p2 - a)
) >= 0;
};
/// <summary>
/// Determines whether a point is inside the triangle formed by given Vector3s
/// </summary>
static Func<Vector3, Vector3, Vector3, Vector3, bool> PointInTriangle = (p, a, b, c) => {
return SameSide(p, a, b, c) && SameSide(p, b, a, c) && SameSide(p, c, a, b);
};
/// <summary>
/// Determines whether triangle of Vector3s one intersects triangle of Vector3s two. (Note: Does not check the other way around.
/// Presumes both are on the same plane.)
/// </summary>
static Func<Vector3[], Vector3[], bool> TriangleInTriangle = (one, two) =>
PointInTriangle(one[0], two[0], two[1], two[2]) ||
PointInTriangle(one[1], two[0], two[1], two[2]) ||
PointInTriangle(one[2], two[0], two[1], two[2])
;
/// <summary>
/// Determines whether either of two triangles intersect the other. The one exception would be a case where the triangles overlap,
/// but do not intersect one another's points. This would require segment intersection tests.
/// </summary>
static Func<Vector3[], Vector3[], bool> TrianglesIntersect = (one, two) =>
TriangleInTriangle(one, two) || TriangleInTriangle(two, one)
;
/// <summary>
/// Organizes a dictionary of triangle indices to average distances by distance, with the furthest at the front.
/// </summary>
static Func<Vector3[], int[], Dictionary<int, float>> OrderByHeight = (vertices, triangles) => {
Dictionary<int, float> height = new Dictionary<int, float>();
for(int i = 0; i < triangles.Length; i += 3)
height.Add(i / 3, yHeight(vertices[triangles[i]], vertices[triangles[i+1]], vertices[triangles[i + 2]]));
return height;
};
/// <summary>
/// Sorts a dictionary of triangle indices to average heights, to a list of the same key value pairs, with the pairs of highest height at the front.
/// </summary>
static Func<Dictionary<int, float>, List<KeyValuePair<int, float>>> SortByHeight = (height) => {
List<KeyValuePair<int, float>> orderedHeight = height.ToList();
orderedHeight.Sort(
delegate(KeyValuePair<int, float> a, KeyValuePair<int, float> b) {
return a.Value > b.Value ? -1 : 1;
}
);
return orderedHeight;
};
/// <summary>
/// Determines whether a provided triangle, in KeyValuePair form of triangle to average distance, is in front of all triangles in a list of pairs.
/// </summary>
static Func<KeyValuePair<int, float>, List<KeyValuePair<int, float>>, Vector3[], bool> IsInFront = (pair, top, vertices) => {
foreach (KeyValuePair<int, float> prevPair in top) {
if (TrianglesIntersect (
ProjectToY (
new Vector3[]{ vertices [pair.Key * 3 + 0], vertices [pair.Key * 3 + 1], vertices [pair.Key * 3 + 2] }
),
ProjectToY (
new Vector3[] {
vertices [prevPair.Key * 3 + 0],
vertices [prevPair.Key * 3 + 1],
vertices [prevPair.Key * 3 + 2]
}
))) {
return false;
}
}
return true;
};
/// <summary>
/// Given a list of triangle indices to average heights, sorted by height with highest first, finds the triangles which are unoccluded by closer triangles.
/// </summary>
static Func<List<KeyValuePair<int, float>>, Vector3[], List<KeyValuePair<int, float>>> GetUnoccludedTriangles = (orderedHeight, vertices) => {
List<KeyValuePair<int, float>> top = new List<KeyValuePair<int, float>> ();
foreach (KeyValuePair<int, float> pair in orderedHeight) {
if(IsInFront(pair, top, vertices))
top.Add(pair);
}
return top;
};
/// <summary>
/// Converts indices of triangles to indices of the first vertex in the triangle.
/// </summary>
static Func<List<KeyValuePair<int, float>>, int[], int[]> ConvertTriangleIndicesToVertexIndices = (top, triangles) => {
int[] topArray = new int[top.Count * 3];
for(int i = 0; i < top.Count; i++) {
topArray[i * 3 + 0] = triangles[top[i].Key * 3 + 0];
topArray[i * 3 + 1] = triangles[top[i].Key * 3 + 1];
topArray[i * 3 + 2] = triangles[top[i].Key * 3 + 2];
}
return topArray;
};
//This seems to work well; but could work better. See to testing it thoroughly, and making it as functional, bulletproof, and short as possible.
/// <summary>
/// Determines the unoccluded "top" of a mesh of triangles, and returns it.
/// </summary>
static Func<Vector3[], int[], int[]> FindTop = (vertices, triangles) => {
return ConvertTriangleIndicesToVertexIndices(
GetUnoccludedTriangles(
SortByHeight(
OrderByHeight(vertices, triangles)
),
vertices),
triangles);
};
您需要使用来自网格的原始数据调用 FindTop 函数来查找 top-side 表面。这个算法有十几种不同的方法可以在其他情况下被扭曲,所以我试着很好地评论它。希望它能在将来帮助其他人。
当我绝对可靠地确信这在所有合理的情况下都有效时,我会接受这个作为答案;但任何有一点建议的人的进一步意见总是受欢迎的。
这就是我最终的结果。我退后一步,考虑我正在尝试做的事情的性质,即只改变顶面;以及有效的约束,这是我需要做的,而且很多都是在 CPU 方面。我还认为我之前的回答中的很多 material 都是多余的,因为我在程序构建网格时已经完成了等效操作。
法线始终朝向多边形可见的方向;所以他们是我解决问题的关键。我在单个 LINQ 查询中完成了它,可在此处获得。
public override void GetAppropriateIndices (Mesh mesh)
{
this.indices = new HashSet<int> (mesh.triangles.AsEnumerable ().Distinct ().Where (index =>
mesh.normals [index] == Vector3.up
).ToList());
}
或者,更灵活地,
public override void GetAppropriateIndices (Mesh mesh)
{
this.indices = new HashSet<int> (mesh.triangles.AsEnumerable ().Distinct ().Where (index =>
Vector3.Angle(mesh.normals [index], Vector3.up) < ε
).ToList());
}
其中 ε 是您愿意在角度上容忍的最大回旋余地。从技术上讲,第二个解决方案更好,因为它考虑了浮点错误以及并非每个法线都将精确地与我们所说的一样的事实,在任何语言中,API,或范例。
所有测试都已通过。我有一个程序生成的海洋切片,动画波浪 运行 仅穿过它的表面(减去用于测试目的的战略切口。)我的解决方案在工作中的图片:
从下面看,未应用网格动态的地方:
你可以在这里看到,由于表面下方的网格法线不够接近 Vector3.up,它没有被修改并保持矩形。
最后,当我将 ε 调到 90f 以上时会发生什么:
我不认为这绝对适用于所有情况,但它们确实有效地完成了工作(所有功能现在都只是一个 LINQ 行......怎么样?)和更具可读性。
在我忘记之前,纹理归功于 Textures.com。