I’ve created a simple non-continuous character controller using CalculateDistance and ColliderCast for collision handling and ground checking respectively, and it’s working pretty well so far. However, there is one major issue I encountered related to the way the ‘SurfaceNormal’ is calculated.
The problem is best explained with the following picture.
Even though the surface is too steep to be considered ground, the ColliderCast ‘touches’ the edge of a triangle and decides (based on its SurfaceNormal) that it is a ‘ground hit’. In theory, this could be easily avoided by only processing the closest hit as opposed to all of them, but the problem with this approach is that I still need to detect ground in situations like this:
Where the closest hit is a ‘steep’ triangle, but a ‘ground’ triangle lies directly below.
I realize that this problem is non-existent with convex-hulls or other collision shapes as only one contact is generated (two convex shapes can only generate a single contact), but I would like for my character controller to work well with meshes as well (especially since there is no easy way to convert a concave mesh into multiple convex hulls as far as I know).
In Unity’s documentation on Collision Queries, it’s written that the ColliderKey parameter is a reference to which triangle or quad in the mesh collider was hit. What happens if a vertex/edge is hit instead? Does that mean that the collision query will output multiple hits with different ColliderKey values but the same contact position? If so, is there a way to somehow find the SurfaceNormal within the context of the whole mesh instead of analyzing each triangle in isolation (as it if were a separate collider)?
Is the problem that you would like to find the “true” normal of the hit triangle rather than a normal based on the cast geometry? You want the red normal instead of the green one in the picture?

This is also a problem I have encountered for my character controller. I would like to know both the green and red normals but a collidercast hit gives you only the green one. It would be very helpful if this information would be given out of the box, and also a bool if the hit was an edge hit or not (red and green normal is same or not). Until this gets added I have made some helper methods that can find the red normal:
public interface INormalFilter
{
bool AcceptNormal(float3 normal);
}
private struct AnyNormalFilter : INormalFilter
{
public bool AcceptNormal(float3 normal) => true;
}
public static void GetTrueNormal<T>(in CollisionWorld collisionWorld, T hit, out float3 trueNormal)
where T : struct, IQueryResult
=> TryGetTrueNormal(collisionWorld, hit, out trueNormal, new AnyNormalFilter());
public static bool TryGetTrueNormal<T, U>(in CollisionWorld collisionWorld, T hit, out float3 trueNormal, U normalFilter)
where T : struct, IQueryResult
where U : struct, INormalFilter
{
RigidBody rigidBody = collisionWorld.Bodies[hit.RigidBodyIndex];
ColliderCastHit castHit = Unity.Collections.LowLevel.Unsafe.UnsafeUtility.As<T, ColliderCastHit>(ref hit);
quaternion bodyRotation = rigidBody.WorldFromBody.rot;
ChildCollider leaf;
unsafe
{
Collider* colliderPointer = (Collider*)rigidBody.Collider.GetUnsafePtr();
colliderPointer->GetLeaf(hit.ColliderKey, out leaf);
}
unsafe
{
switch (leaf.Collider->Type)
{
case ColliderType.Quad:
case ColliderType.Triangle:
return QuadTriangleCase(out trueNormal);
case ColliderType.Box:
case ColliderType.Cylinder:
return BoxCylinderCase(out trueNormal);
default:
return AcceptNormal(castHit.SurfaceNormal, out trueNormal);
}
}
unsafe bool QuadTriangleCase(out float3 trueNormal)
{
float3 normal = GetWorldNormal(((PolygonCollider*)leaf.Collider)->Planes[0]);
return AcceptNormal(normal, out trueNormal);
}
unsafe bool BoxCylinderCase(out float3 trueNormal)
{
if (((BoxCollider*)leaf.Collider)->BevelRadius == 0f)
{
return FilterPlanes(out trueNormal);
}
else
{
return AcceptNormal(castHit.SurfaceNormal, out trueNormal);
}
bool FilterPlanes(out float3 trueNormal)
{
var planes = ((PolygonCollider*)leaf.Collider)->Planes;
trueNormal = float3.zero;
bool foundNormal = false;
float bestDot = -1f;
float3 localHitPosition = math.mul(math.inverse(bodyRotation), castHit.Position - rigidBody.WorldFromBody.pos);
for (int i = 0; i < planes.Length; i++)
{
float3 normal = GetWorldNormal(planes[i]);
if (!normalFilter.AcceptNormal(normal)) continue;
if (planes[i].SignedDistanceToPoint(localHitPosition) < 0) continue;
float newDot = math.dot(normal, castHit.SurfaceNormal);
if (newDot > bestDot)
{
trueNormal = normal;
bestDot = newDot;
foundNormal = true;
}
}
return foundNormal;
}
}
bool AcceptNormal(float3 normal, out float3 trueNormal)
{
if (normalFilter.AcceptNormal(normal))
{
trueNormal = normal;
return true;
}
trueNormal = float3.zero;
return false;
}
float3 GetWorldNormal(Plane plane) => math.mul(bodyRotation, plane.Normal);
}
GetTrueNormal() output any true normal, but you can also use TryGetTrueNormal() and provide it with an implementation of INormalFilter to filter out normals you don’t accept.
2 Likes
Wow this is incredibly useful, thanks for sharing your own solution.
Regarding the first question, yes, that’s what I thought initially, but later realized that I didn’t want to completely ignore the normal based on the cast geometry, either. There are instances where I want the green normal (for example I want my character to be able to stand on the tip of a cone), I just want to filter out the ones that are ‘incorrect’ (such as the one in the first picture of my post).
I’m wondering if it’s possible to get the neighbor triangles if an edge/vertex is hit and check if the green normal ‘ranges’ between the 2 or more red normals…
What I did to get neighboring triangles/quads was implement a custom collector: Interface ICollector<T> | Unity Physics | 0.6.0-preview.3
The collector will find all hits along the cast distance. For each triangle/quad collider there will be a hit. In the AddHit() method I first check if a hit was an edge hit by comparing the trueNormal with the SurfaceNormal, if their dot product is close to one it is not an edge hit. If it is an edge hit I check the following hits if they are within a small distance to the edge hit. If they are then they are neighbors.
That sounds pretty good, I’ll definitely try that. I assume the reason this works is because the neighbor triangles are usually processed one after the other?
Either way though, I think you’re right in that it would be pretty handy to have these functions out of the box, and to have more information (edge/vertex hit, etc.) stored in the query output. Thanks again!
I think the ordering is arbitrary, but is should still be possible to process them.
Yes, I’ve seen lots of people looking for this kind of functionality, even many years back. It wasn’t possible with physx and now its sort of possible with custom scripts. A more robust and official solution would be welcomed. I hope the devs sees this and take note. 