0
\$\begingroup\$

As in the title, I have an effect prepped for my game which applies certain derived parameters to a post-processing effect after the player enters a boundary I created with ProBuilder. Finding how far inside the bound the player is; that is, how far the player would have to travel, at minimum, to leave it; would help me scale this effect. It is not an axis-aligned boundary, and its sloped shape is fairly important. Having the effect go full-on on crossing the boundary would be jarring to the player.

I'm tempted to resort to traditional edge testing with this, but I would like to highlight that it will be done at least once per frame on multiple objects, so I would like to keep it as simple, and non-home-rolled, as possible.

The other possibility would be to keep track of time exposed and scale it by that; but this isn't ideal and I'm seriously curious as to whether a penetration-depth-testing method exists out-of-box.


A requested image of my setup: enter image description here It is laterally symmetric, and that is unlikely to ever need to change. However, future volumes are likely to have varying angles and heights. Depth is more-or-less irrelevant, as while I'm using 3D, this game has the physics of a 2D platformer; so you can consider it to have infinite depth if you need to. As you can see on the right, I've added almost nothing to it, other than the MagneticField.cs custom script (which does not even reference the mesh, only whether it's been crossed) and a debug class, TriangleReport.cs, that I'm working on right now.

One thought I had was to get triangles for the top, bottom, left, and right edges, use them to define planes, and then determine the distance from those planes; but if you've got something better I am all ears!

\$\endgroup\$
  • \$\begingroup\$ Can you show us a visual example of what these boundaries look like in context? There are various shortcuts and optimizations we might be able to apply, but they're sensitive to the details of your scene geometry. The most general case can be quite expensive, but many special cases can be very cheaply evaluated. \$\endgroup\$ – DMGregory Sep 10 '18 at 22:40
  • \$\begingroup\$ @DMGregory Done! I hope it's helpful. \$\endgroup\$ – Michael Eric Oberlin Sep 10 '18 at 22:53
  • \$\begingroup\$ Come to think of it, it is also likely to always be z-axis-aligned; I cannot speak for anything else, though. Additionally, these will pretty much always be hexahedrons. \$\endgroup\$ – Michael Eric Oberlin Sep 10 '18 at 22:56
  • \$\begingroup\$ Can we summarize the problem then as "determine the distance between a 2D point and the closest edge of a trapezoid"? Or do you have non-trapezoidal shapes? \$\endgroup\$ – DMGregory Sep 10 '18 at 22:57
  • \$\begingroup\$ @DMGregory In this instance I don't expect to, so I think you're right. It's an approximation of a cone, so it should always be a trapezoid, unless I come up with a really weird situation. \$\endgroup\$ – Michael Eric Oberlin Sep 10 '18 at 23:20
0
\$\begingroup\$

So this is what I've got going. On account of the inquiries of @DMGregory, I managed to file down exactly what I needed to test for. It's likely that I'll do something a little different, but this seems to work:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class TriangleReport : MonoBehaviour {

GameManager manager;

Mesh mesh;

int[] triangles;
Vector3[] vertices;
Vector3[] normals;

Plane Left, Right, Top, Bottom;

void Awake() {
    manager = GameObject.Find("/Game Manager").GetComponent<GameManager>();
}

///<summary>
///I will only ever need to worry about the top, bottom, left, and right of the mesh bounds, which will be z-axis aligned.
///That said, I can compare the normals of the triangles to the Left/Up/Right/Down normals and find the ones that best fit. Since
///one triangle defines a plane, this will be all I need to simply do a minimum of plane distances.
///</summary>
void Start () {
    mesh = GetComponent<MeshFilter>().mesh;
    triangles = mesh.triangles;
    normals = mesh.normals;
    vertices = mesh.vertices;

    Vector3[] RightVerts = GetFaceAligned(vertices, triangles, normals, Vector3.right);
    Vector3[] LeftVerts = GetFaceAligned(vertices, triangles, normals, Vector3.left);
    Vector3[] TopVerts = GetFaceAligned(vertices, triangles, normals, Vector3.up);
    Vector3[] BottomVerts = GetFaceAligned(vertices, triangles, normals, Vector3.down);

    //Get normal of the triangle via a cross product (either direction will do)
    Vector3 NormalRight = Vector3.Cross(RightVerts[1] - RightVerts[0], RightVerts[2] - RightVerts[0]).normalized;
    Vector3 NormalLeft = Vector3.Cross(LeftVerts[1] - LeftVerts[0], LeftVerts[2] - LeftVerts[0]).normalized;
    Vector3 NormalTop = Vector3.Cross(TopVerts[1] - TopVerts[0], TopVerts[2] - TopVerts[0]).normalized;
    Vector3 NormalBottom = Vector3.Cross(BottomVerts[1] - BottomVerts[0], BottomVerts[2] - BottomVerts[0]).normalized;

    Right = new Plane(RightVerts[0], NormalRight);
    Left = new Plane(LeftVerts[0], NormalLeft);
    Top = new Plane(TopVerts[0], NormalTop);
    Bottom = new Plane(BottomVerts[0], NormalBottom);
}

///<summary>
///Find the triangle that best matches the provided normal, and return it
///</summary>
Vector3[] GetFaceAligned(Vector3[] vertices, int[] triangles, Vector3[] normals, Vector3 bestNormal) {
    int winner = -1;
    float winningDot = System.Single.NegativeInfinity;

    for(int i = 0; i < triangles.Length; i += 3) {
        Vector3 normal = (normals[triangles[i]] + normals[triangles[i+1]] + normals[triangles[i+2]])/3f;
        float dot = Vector3.Dot(normal, bestNormal);

        if(dot > winningDot) {
            winningDot = dot;
            winner = i;
        }
    }

    return new Vector3[3]{
        vertices[triangles[winner]],
        vertices[triangles[winner + 1]],
        vertices[triangles[winner + 2]]
    };
}

// Update is called once per frame
void OnTriggerStay (Collider c) {
    Vector3 pos = transform.InverseTransformPoint(manager.robot.transform.position);
    float distance = Mathf.Min(Bottom.GetDistanceToPoint(pos), 
        Mathf.Min(Top.GetDistanceToPoint(pos),
        Mathf.Min(Left.GetDistanceToPoint(pos),
        Right.GetDistanceToPoint(pos))));
    Debug.Log(distance);
}
}

That said, what I do is compare each triangle's effective normal to the direction I'm looking for, and select the one that has the highest (and most congruent) dot product. This would be the one that "best" faces that direction.

Since all a plane needs is three vertices and a normal, and in this instance either normal would work, all I have to do to get a plane is take the cross product of two vectors along the triangle.

Plane has a convenient GetDistanceToPoint method which will provide the distance to a Vector3 to me. To determine the shortest path out, or at least, its length, all I have to do is take the minimum.

Looking back, there is easily a better way to find the minimum of four items... but this works well as a P.o.C.

@DMGregory also pointed out that I'm effectively, at least in this instance, trying to find the distance to the edge of a 2D trapezoid; but this might not always be true for this project. (We'll see.) I hope this helps someone.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.