Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

SphereCast & CapsuleCast RaycastHit.normal is not the surface normal as the documentation states...

Discussion in 'Scripting' started by lordofduct, Oct 22, 2014.

  1. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    So the documentation for RaycastHit says that the 'normal' property is the normal of the surface hit by the cast:
    http://docs.unity3d.com/ScriptReference/RaycastHit-normal.html

    But this actually isn't true in the case of SphereCast and CapsuleCast (underneath SphereCast is actually a CapsuleCast, so DotPeek enlightened me to).

    Instead the normal returned is the inverse normal of the capsule/sphere that hit the normal.

    This is fine when hitting a flat surface head on, as the surface normal, and the inverse normal of the sphere/capsule are identical.

    But if you cast at an edge, and the sphere/capsule hit slightly off. You get something like:


    From what I can tell this is the expected value to come back as per PhysX documentation. Which basically just says to me Unity just misdocumented the RaycastHit.normal in their documentation, and that it's not actually a bug. Which would make sense, because RaycastHit in the case of Raycast DOES return the surface normal. It just doesn't for Capsule/Sphere cast.

    Furthermore, this normal can be very useful for various things.

    BUT, the actual surface normal would be useful as well.

    Currently I have a function to get the surface normal. What I do is check if the hit collider was a MeshCollider and calculate the surface normal based on the mesh data. If it's not a MeshCollider than I do a short Raycast at the surface to calculate the surface normal. The latter of which means 2 consecutive casts each time AND a slight potential for error.

    You can see this here:
    Code (csharp):
    1.  
    2.         public static Vector3 RepairHitSurfaceNormal(RaycastHit hit, int layerMask)
    3.         {
    4.             if(hit.collider is MeshCollider)
    5.             {
    6.                 var collider = hit.collider as MeshCollider;
    7.                 var mesh = collider.sharedMesh;
    8.                 var tris = mesh.triangles;
    9.                 var verts = mesh.vertices;
    10.  
    11.                 var v0 = verts[tris[hit.triangleIndex * 3]];
    12.                 var v1 = verts[tris[hit.triangleIndex * 3 + 1]];
    13.                 var v2 = verts[tris[hit.triangleIndex * 3 + 2]];
    14.  
    15.                 var n = Vector3.Cross(v1 - v0, v2 - v1).normalized;
    16.  
    17.                 return hit.transform.TransformDirection(n);
    18.             }
    19.             else
    20.             {
    21.                 var p = hit.point + hit.normal * 0.01f;
    22.                 Physics.Raycast(p, -hit.normal, out hit, 0.011f, layerMask);
    23.                 return hit.normal;
    24.             }
    25.         }
    26.  
    I was wondering what the rest of you do? If you use a similar method to me, or if maybe there's a BETTER option?
     
    Xriuk, BogdanDude, noio and 3 others like this.
  2. rageingnonsense

    rageingnonsense

    Joined:
    Dec 3, 2014
    Posts:
    99
    I just noticed this behavior, and it sort of makes sense. I mean I see the logic in that, but I see normals that would be impossible from a capsulecast. I also see hit points that are impossible (for instance, my hit point doesn't even rest anywhere on the collider, just close to it)

    Either way, I am pretty sure the problem I have is the exact same issue. Been using SphereCasts and CapsuleCasts for a long while now, and never had this problem until today.

    I'll try your code; it looks solid. The only thing I would do different is make it an extension method.
     
  3. Lord Emperor

    Lord Emperor

    Joined:
    Sep 14, 2015
    Posts:
    4
    You can raycast against the desired collider (hit.collider.Raycast) instead of a generic Physics.Raycast. This should be faster because it only has to test against the one collider.
     
  4. Xriuk

    Xriuk

    Joined:
    Aug 10, 2018
    Posts:
    21
    Sorry to necro this, but the issue is still actual, there is a little problem with the code above that on edges of BoxColliders raycasting by using the inverse of the normal of the hit can lead to the wrong normal being detected.
    In the example below I SphereCast down on an edge and I get a random normal (red), when applying the function above to the hit I get a wrong normal (blue), since I was SphereCasting down I would expect a normal towards up in this case.
    upload_2022-1-10_10-41-22.png

    I've solved this with the code below (also implemented @Lord Emperor 's suggestion and made it an extension method like @rageingnonsense suggested):

    Code (CSharp):
    1. /// <summary>
    2. /// Calculates the surface normal for CapsuleCast and SphereCast
    3. /// </summary>
    4. /// <param name="hit">original hit</param>
    5. /// <param name="dir">original direction of the raycast</param>
    6. /// <returns>correct normal</returns>
    7. /// <remarks>https://forum.unity.com/threads/spherecast-capsulecast-raycasthit-normal-is-not-the-surface-normal-as-the-documentation-states.275369/</remarks>
    8. public static Vector3 GetCorrectNormalForSphere(this RaycastHit hit, Vector3 dir) {
    9.     if(hit.collider is MeshCollider) {
    10.         var collider = hit.collider as MeshCollider;
    11.         var mesh = collider.sharedMesh;
    12.         var tris = mesh.triangles;
    13.         var verts = mesh.vertices;
    14.  
    15.         var v0 = verts[tris[hit.triangleIndex * 3]];
    16.         var v1 = verts[tris[hit.triangleIndex * 3 + 1]];
    17.         var v2 = verts[tris[hit.triangleIndex * 3 + 2]];
    18.  
    19.         var n = Vector3.Cross(v1 - v0, v2 - v1).normalized;
    20.  
    21.         return hit.transform.TransformDirection(n);
    22.     }
    23.     else {
    24.         RaycastHit result;
    25.         hit.collider.Raycast(new Ray(hit.point - dir * 0.01f, dir), out result, 0.011f);
    26.         return result.normal;
    27.     }
    28. }
     
    Seith likes this.
  5. tcz8

    tcz8

    Joined:
    Aug 20, 2015
    Posts:
    504
    I don't think it's random. It's returning the direction to the sphere's center on hit. The surface normal is already accessible in other ways. It's very useful for character controllers and anything where you want to know the direction to return force to an "impactor".

    Getting the center of the sphere on impact:
    Code (CSharp):
    1. Vector3 center = hit.point + sphereCastRadius * hit.normal
     
    Last edited: Apr 22, 2022