Michael Macha
Michael Macha

Reputation: 1781

How to find only the "top" of a mesh in Unity

I need to determine the vertices of a polytope (mesh), in Unity, that make up its "top"; that is, the furthest points along the y-axis.

The reason for this is that I am creating a procedural environment with a number of mesh dynamics active in it; one of them shapes the ocean to a waveform. It works beautifully, but also contorts the bottom of the water, which is not ideal.

I feel like the answer may involve a projection of some kind, but I'm not certain how.

Upvotes: 4

Views: 5945

Answers (3)

Michael Macha
Michael Macha

Reputation: 1781

Please Note Before Reading: This was my original method, which worked well in practice, but there turned out to be a much simpler and more readable solution which is posted down the line.

First and foremost, let me emphasize that I am still Unit Testing this code, and it may have bugs in it. So far, it's been good.

Here's what I've done.

I wrote it up on the whiteboard and noticed that I'm projecting orthogonally down y. Thus, the first thing I needed to do was get the average y-value for each triangle.

(Observe that this means that this will be useless in the case of self-intersecting meshes; to deal with those, you'll have to come up with a better definition for what "top" actually means.)

After that, I sorted the triangles according to the average y values, with the highest y-values at the front of the list.

After that, it was a sorting problem! For each triangle, if it intersected none of the triangles which I had previously kept, then I added it to the list of top-side triangles. I used a point-in-triangle test for this. If it did intersect, then it was beneath one of my top-side triangles and could be skipped.

The one other case in which this would not work would be instances of triangles overlapping, but not intersecting, other triangles; this means any case where the edges cross each other, but none of the vertices are "inside" the bounds of the other triangle. Kind of like a Star-of-David shape; this code will not detect them as crossing.

As I said, I'm still unit testing this for forseeable circumstances; but my code is as follows. I went with a functional paradigm because it keeps things easy to follow, and works fine for modern day gaming software. Note that you will want to memoize this for any kind of mesh dynamic; you do not want to be scanning through every single triangle, every time you want to alter the mesh!

    /// <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);
    };

You'll want to call the FindTop function, with raw data from the mesh, to find the top-side surface. There are a dozen different ways this algorithm could be contorted for other circumstances, so I've tried to comment it well. Hopefully it will help others out in the future.

When I'm absolutely and reliably convinced that this works under all reasonable circumstances, I'll accept this as an answer; but further input, from anybody who has a dime of advice, is always welcome.

Upvotes: 0

Michael Macha
Michael Macha

Reputation: 1781

This is what I ended up going with. I stepped back and considered the nature of what I was trying to do, which was to alter only to the top faces; along with the effective constraints, which were that I needed to do it, and a lot of it, on the CPU side. I also considered that much of the material in my previous answer was redundant, as I had already done equivalent operations when procedurally building the mesh.

Normals always face towards the direction from which the polygon is visible; so they were the key to my solution. I accomplished it in a single LINQ query, available here.

    public override void GetAppropriateIndices (Mesh mesh)
    {
        this.indices = new HashSet<int> (mesh.triangles.AsEnumerable ().Distinct ().Where (index => 
            mesh.normals [index] == Vector3.up
        ).ToList());
    }

Or, more flexibly,

    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());
    }

where ε is the maximum amount of leeway you are willing to tolerate on angle. Technically the second solution is better, as it takes into account floating point error and the fact that not every normal is going to be precisely what we said it should be, in any language, API, or paradigm.

All testing has passed. I have a procedurally generated slice of ocean, with animated waves running through only its surface (minus the strategic cut outs for testing purposes.) Pictures of my solution at work:

Wave mesh dynamic applied to an almost rectangular mesh

And from below, where the mesh dynamic is not applied:

Since the bottom vertices have normal facing dramatically away from Vector3.up, they never change.

You can see here that since the normal of the mesh, below the surface, is not close enough to Vector3.up, it is not modified and remains rectangular.

Lastly, what happens when I dial my ε up to above 90f:

At ε above 90f, the sides are also shifted, so they move with the wave form as well!

I don't imagine that this will be suitable for absolutely every case, but they do efficiently get the job done (all of the functions are now just a single LINQ line... how 'bout that?) and are much more readable.

Before I forget, texture credit goes to Textures.com.

Upvotes: 1

Doh09
Doh09

Reputation: 2385

Disclaimer: I haven't worked with meshes in this way before, but here are 2 ways of thinking that I believe might bring you further towards a fully working solution.

Possible solutions:

Use the Mesh.Bounds property.

Although I suspect this only works on flat surfaces.

Source: https://docs.unity3d.com/ScriptReference/Mesh-bounds.html

Use Mesh.Vertices and, depending what axis you are using as top (y axis), use the 2 other axes (x and z) in each array index to calculate what is the highest point in that particular part of the mesh. Make a whole collection of these and tadah, you have the surface.

Mesh.Vertices returns a Vector3 array.

Imagine you have these coordinates.

Vector3 (443, 31, 543);
Vector3 (443, 32, 543);
Vector3 (443, 33, 543);
Vector3 (443, 34, 543);
Vector3 (443, 35, 543);

The highest coordinate here would be the one where y is 35, assuming that the positive y axis is what you consider the top.

It might be necessary to group certain vertices together, depending on vertice numbers you are given, so that Vector3 (443, 35, 543); and Vector3 (443.1, 35, 543.2); are in the same comparison.

Source: https://docs.unity3d.com/ScriptReference/Mesh-vertices.html

Hope this can help you.

Upvotes: 1

Related Questions