I am using a spherecast to detect ground in 3D scenes. Sometimes the spherecast contacts the edge/vertex of a collision shape, and this returns a normal that is interpolated between the normals of the surrounding faces of that collision shape. I would like to know the normals of the faces from which this interpolation is derived because this can give me more accurate information about the ground around me.

One solution I think will work is using additional raycasts that fire in the opposite direction of the interpolated normal. To do this, one can start at the hit.point, and then move the raycasts a small amount along the direction that is perpendicular to the interpolated normal. This is trivial to do in 2D using Vector2.Perpendicular. However, I am having a difficult time figuring out how to make this work in 3D because many vectors may be perpendicular to a given vector. I know that I can use Vector3.Cross to find this perpendicular direction, and that one of the vectors passed to this function will be the interpolated normal. But what is the second direction I need?

Here is an image that shows whatâ€™s happening in 2D. The green circle casts itself downward (in red), contacting the edge of the platform. The interpolated hit.normal is shown below in green, with its perpendicular in magenta. Using this magenta vector, we can raycast (in red) in the opposite direction of the green vector and find the top and side faces of the gray collider, obtaining more information about the ground around the circle.

Well, actually thereâ€™s a bit more than a lot, thereâ€™s an infinite amount of Vectors which are perpendicular. If you Dot Product two Vectors and they equal 0, then they are perpendicular. You just have to invert the equation to get a formula for perpendicular Vectors.

AFAIK the normal of the hit point of a sphere cast is simply the direction from the hitpoint to the sphereâ€™s center at the point where the spherecast hits. So the normal would always be the normal of the tangent plane at the intersection point on the sphere or the spherecast. That is regardless of whether you hit a corner, edge or face.

If you just want an arbitrary vector that is perpendicular to a given one, and you donâ€™t care about which direction, just choose one. Use Vector3.right for example. If you want it to be more robust, you can check if the angle between them is sufficient, and if not, then pick anything else.

private static Vector3 ArbitraryPerpendicular(Vector3 original)
{
Vector3 another = Vector3.right;
if (Mathf.Abs(Vector3.Dot(original, another) > 0.9f)
another = Vector3.forward;
return Vector3.Cross(original, another);
}

Another approach would be just to pick any point(s) that are near the center of the sphere, and cast outward from there instead of from the center of the sphere. Start from 0.25f * Random.onUnitSphere for example.

This was the important piece of information that I needed. Many sources claim, as I did in the original post, that the returned edge/vertex normal is interpolated, but the actual normal is indeed the direction from the contact point to the centroid at the time of contact. The reason it is easy to find the appropriate perpendicular vector in 2D is because we are only dealing with the XY plane, as the forward (Z) direction is constant; in other words, Vector2.Perpendicular is really like using Vector3.Cross where one of the vectors passed to the function is guaranteed to be Vector3.Forward.

In 3D, we can assume that the hit.normal is the â€śupâ€ť vector of a new 3D plane, and the â€śconstantâ€ť forward vector will simply be the vector that is perpendicular to this â€śupâ€ť vector and the downward raycast vector:

The way I think about this is like â€śrebuildingâ€ť the XY plane every time we detect a contact, but where the Y component of the plane is represented by the hit.normal. Using the downward raycast vector to define the Z component of the plane makes sense because it is going to be the same every time, regardless of the casting GameObjectâ€™s rotation (you can simply do rotation * Vector3.down; or -transform.up).

I appreciate everyone who commented and tried to help me solve this.