Search Unity

  1. Are you interested in providing feedback directly to Unity teams? Sign up to become a member of Unity Pulse, our new product feedback and research community.
    Dismiss Notice

Help Wanted Sphere collider receives strong impulse on contact with mesh collider edge

Discussion in 'DOTS Physics' started by KwahuNashoba, May 14, 2020.

  1. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    As you can observe from the picture 1 and 2, sphere collider receives strong vertical impulse while colliding with an edge, while having normal impulse in the middle of polygon. Previewed in grey is static mesh collider.

    upload_2020-5-14_7-38-53.jpeg

    When the ball rolls down only by gravity pull, this is barely noticeable, represented as tiny hiccups. But as soon as I add either linear or angular velocity, the ball starts jumping with a greater contact velocity resulting in larger jumps.

    Is this an expected behavior, does it have something to do with DOTS Physics being cache-less? If the answer is yes, does it mean I'll need to alter the results after
    SimulationCallbacks.Phase.PostCreateContacts
    ? Any advice much appreciated.
     
  2. steveeHavok

    steveeHavok

    Joined:
    Mar 19, 2019
    Posts:
    480
    KwahuNashoba likes this.
  3. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Thanks Stevee, LOTS of useful information on links and talks. I have a progress, so I would much appreciate validation of my approach since I believe there is a better way that I couldn't find. What I wanted to do is the same thing as in ModifyNarrowPhaseContacts example but, since I don't have flat surface, I need to correct normal on polygon level.

    Here is the approach I use:
    1. Generate entity query that schedules IContactsJob instance callback for each entity with surface modificatior
    Code (CSharp):
    1.  
    2.         var m_BuildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
    3.         var modifiers = m_ContactModifierGroup.ToComponentDataArray<ModifySpeculateveCcdContactPointNormal>(Allocator.Temp);
    4.         // schedule a job for each surface that should modify interaction
    5.         for (int i = 0; i < modifiers.Length; ++i)
    6.         {
    7.             var surfaceRBIdx = m_BuildPhysicsWorld.PhysicsWorld.GetRigidBodyIndex(modifiers[i].surfaceEntity);
    8.  
    9.             SimulationCallbacks.Callback callback = (ref ISimulation simulation, ref PhysicsWorld world, JobHandle inDeps) =>
    10.             {
    11.                 return new ModifyNormalsJob
    12.                 {
    13.                     m_SurfaceRBIdx = surfaceRBIdx,
    14.                     world = m_BuildPhysicsWorld.PhysicsWorld
    15.                 }.Schedule(simulation, ref world, inDeps);
    16.             };
    17.             m_StepPhysicsWorld.EnqueueCallback(SimulationCallbacks.Phase.PostCreateContacts, callback);
    18.         }
    2. Inside a job, cast ray from dynamic body to contact point in order to get surface normal
    Code (CSharp):
    1. public float3 RaycastNormal(float3 RayFrom, float3 RayTo)
    2.         {
    3.             RaycastInput input = new RaycastInput()
    4.             {
    5.                 Start = RayFrom,
    6.                 End = RayTo,
    7.                 Filter = new CollisionFilter()
    8.                 {
    9.                     BelongsTo = ~0u, // all 1s, so all layers, collide with everything
    10.                     CollidesWith = 1u, // track layer
    11.                     GroupIndex = 0
    12.                 }
    13.             };
    14.  
    15.             Unity.Physics.RaycastHit hit = new Unity.Physics.RaycastHit();
    16.             bool haveHit = world.CastRay(input, out hit);
    17.             if (haveHit)
    18.             {
    19.                 return hit.SurfaceNormal;
    20.             }
    21.             return float3.zero;
    22.         }
    3. Set surface normal as the new normal of ModifiableContactHeader
    Code (CSharp):
    1. public void Execute(ref ModifiableContactHeader contactHeader, ref ModifiableContactPoint contactPoint)
    2.         {
    3.             bool bodyAIsSurface = contactHeader.BodyIndexPair.BodyAIndex == m_SurfaceRBIdx;
    4.             bool bodyBIsSurface = contactHeader.BodyIndexPair.BodyBIndex == m_SurfaceRBIdx;
    5.  
    6.             // ignore contacts where m_SurfaceRBIdx does not take part in
    7.             if (!(bodyAIsSurface || bodyBIsSurface)) return;
    8.  
    9.             float3 dynamicBodyPosition = world.GetPosition(bodyBIsSurface ? contactHeader.BodyIndexPair.BodyAIndex : contactHeader.BodyIndexPair.BodyBIndex);
    10.  
    11.             var newNormal = RaycastNormal(dynamicBodyPosition, contactPoint.Position);
    12.             distanceScale = math.dot(newNormal, contactHeader.Normal);
    13.             contactHeader.Normal = newNormal;
    14.        
    15.             contactPoint.Distance *= distanceScale;
    16.         }

    EDIT: It turned out that the problem described bellow was that rays were a bit to short, so I extended them and I get the hit on the "Inside" part of collider.

    I even struggle to make above approach work because it looks like contact points become somewhat inverted during this process as you can see in following pictures. Yellow lines are rays and red lines are surface normals.

    "Inside" of mesh collider:
    upload_2020-5-25_21-1-26.png

    "Outside" of mesh collider:
    upload_2020-5-25_21-2-27.png
     
    Last edited: May 25, 2020
    GliderGuy likes this.
  4. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Hmmm...it looks like I solved the problem with normals but that did not solve my bounciness problem because impact is still calculated by the solver and I just redirected it's resulting direction. It does not look like I can do much about it at this phase and here is why I think so.

    When calculating impulse magnitude of the collision event, physics sums impact of all the estimated contact points in a next way:
    1. Projects each vector that stretches from center of mass to the contact point (cyan lines) on the corresponding normal (red lines) via dot product
    2. Sums this projected magnitudes
    3. Divides this sum with the impact force
    I'm not sure how the integration goes in order to compute final impact points (blue lines) but it looks like that, because the transversal edge of forthcoming polygon has more than one impact point, it adds up a lot of impact force and hence the bounce.

    I believe that correcting contact point distance (length of green arrows) might help resolving this issue, but I have no idea how and to what extent, ATM.

    upload_2020-5-26_1-56-19.png
     
    GliderGuy likes this.
  5. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    @steveeHavok or anyone else from the team that feels like he can provide any help, I wold really appreciate some background info regarding this matter :(

    I'm trying to figure out what is the root cause of this "problem" and It feels like I'm kind of beating around the bushes but cant make the lizard go out of it...I love lizards.

    Here are some things I'm struggling to see the clear picture about by examining the source code:
    1. Is the solver integrating all the
      ContactPoint
      s across all the
      ContactHeader
      s or is it finding the
      ContactHeader
      that contains closest
      ContactPoint
      to the impact (smallest Distance) and integrate all the contact points in that header?
    2. It seams like all the
      ContactHeader
      s are vertex contacts (have value of 1 for
      NumContacts
      ). Is this something intentional? It feels, by the picture above, that there are 3 contact points belonging to the same edge, what kind of makes me thinking that there is something to it, that I could resolve it by dropping header if it's an edge, or removing some contact points.
    Just a note that I'm currently moving the ball via angular velocity, but I've tried with linear at some point and it behaved in a similar manner.
     
    GliderGuy likes this.
  6. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    1. Yes, the solver looks at every contact point of every header, it doesn't choose anything
    2. For a sphere collider that is expected, in your particular case you have a mesh with triangles, and every sphere-triangle pair produces a contact header (because headers work on collider key pairs) and a contact point (because all you need to separate a sphere and a triangle is a single contact point).

    I took a brief look at what you are doing with your fixup of contact points, and casting a ray towards the point does produce a better normal, but not good enough. Could you instead try to cast the ray (or even better the collider) along the direction of movement, and if no hit it's obviously a ghost plane, but if there's a hit that should give you a much better normal. Does that sound like something you would like to try?
     
  7. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Thanks for the response, I guessed that it is because of the way the sphere is represented and I even thought about using the "real" polygonized sphere but decided to try make it work this way for the sake of efficiency.

    I'll try literally anything since I'm kind of loosing ideas of how to approach this any more o_O

    What is the idea behind collider cast?

    If I cast the collider along the linear velocity vector, get the hit, and then use its point to somehow decide if I should disable contact point, I might end up with no "supporting" contacts on flat segments so the ball will fall through the mesh collider, right?

    On the other hand, if I use that hit point of collider cast and then set all the contact points calculated by the narrow phase to be at that point, an probably adjust distance somehow, time to impact might get to long so the narrow phase impulse estimation ignores it.

    Or I might be getting your idea wrong.

    What is REALLY interesting observation, and I've been observing this thing A LOT, is that it looks like it uses orientation of the edge relative to the linear velocity, so it gets bumped only on perpendicularish edges. This is just an really intuitive observation and I could not really find part of code that describes this behavior.

    What do you think about this idea:
    1. Find the normals that are opposing linear velocity vector (those should be normals of forthcoming edge)
    2. Align those normals with the mesh surface in the direction of linear velocity
     
  8. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    The idea behind collider cast is exactly what you described in the end - to detect normals that are opposing the linear velocity vector and align them. You shouldn't even disable a contact point, just fix up the normal so that it's not "ghost". Also you won't be getting fall through the ground because your linear velocity vector incorporates some downwards velocity (coming from gravity) so the direction will be pointed downwards to some extend and it will pick up ground hits.
     
  9. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Great! I'll play with it and come back with the results.

    One thing before I immerse in this adventure...it seams to me that we might not be watching this from the same point of view so I would like to align them to avoid pitfalls.

    I'm scheduling a job that handles this whole welding logic in a job that is scheduled like so
    Code (CSharp):
    1. m_StepPhysicsWorld.EnqueueCallback(SimulationCallbacks.Phase.PostCreateContacts, callback)
    so I already have all the contact headers/points and I'm doing raycasts inside that job only to get the surface normals (which I'll need anyway for gravity factor correction but that's other matter). So what is idea with collider cast at that point in time, I thought about just using the pre-calculated normals from narrow phase and not doing raycasts at all?

    This should work if I leave the contacts as they are, but my whole idea about resolving this is actually about adjusting contact normals/distances - the green arrows - in order to avoid "ghosts", please point me in correct direction if I've approached this whole thing from the wrong angle.
     
  10. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    Maybe you don't even need a full collider cast, but just a simple "sub-integration" to the normal and then do another sphere vs. whatever collider is there to fixup the normal... The normal is what is bothering you here, you don't really need to touch the contact points I believe.

    But before that, I just realized, have you looked at the https://github.com/Unity-Technologi...mos/5. Modify/ModifyNarrowphaseContacts.unity demo in the samples? And more importantly, this script https://github.com/Unity-Technologi...Scripts/ModifyNarrowphaseContactsBehaviour.cs? It is doing what you are trying to achieve, and at the exact same point in time you are doing it.
     
  11. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    I believe you are correct, when I say contact point, I usually refer to normal in contact header originating from contact point, since there is single contact point per contact header so the contact point only really adds length to the normal.

    Yes, I used narrow phase modification sample as a starting point. On the last image above, I did practically the same thing, correcting normals to align with polygon normals and scaling them by projecting original normal to the new, perpendicular to surface, normal (dot product basically). The only thing I did differently was raycast, because my surface is not flat so I couldn't just assign vector.up.
    But it maid the things even worse, since now there are no two opposing vectors (linear velocity and impact vector) thus making resulting impulse gaining a bit steeper angled and propelling the ball higher in the air.
     
  12. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    Hi, regarding ModifyNarrowphaseContactsBehaviour, I've implemented a partial solution based on that. I posted about it in this thread. The main issues with it are that it doesn't stop bumps between adjacent colliders and that I found the balls didn't play nicely with concave colliders such as a tunnel. Also you have to alter the Physics package to make ConvexHull public.
     
  13. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    I think I came across your solution but I wanted to avoid modifying physics so I ended up casting ray instead of accessing underlying data, you can see code sample above in one of my answers. But that made it even worse in my case if you look at the previous answer. Have you managed to make it work somehow? Something else than just correcting normal angle/distance?
     
  14. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    Unfortunately no, this is the best I could do. Frankly, I'm out of my depth here.
     
    KwahuNashoba likes this.
  15. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    I hope I understand the issue properly and that content below will unblock you.

    First of all I'll try to reduce your case to the image(s) below
    Welding1.png
    a is contactPoint.Position (It's a world position of contact point on body A)
    b is contact point on triangle 2 of body B
    c is position of body A

    So first version of your ray that didn't have a hit was from c to a and when you expanded it you managed to hit body b. Luckily for sphere a, b and c are colinear so extended ray was hitting the mesh in b (or around b due to numerical inaccuracies.

    Next thing with the ray is that it does a cast on a world level so the hit can happen on either triangle 1 or triangle 2. It can be considered that as if triangle being hit is random from those two triangles. So the resulting normal may be the blue one which means that penetration can happen and then penetration recovery can bounce the sphere off. If b is a vertex shared between the triangles the result can just be worse.

    It's easy to create a situation where wrong triangle being hit can result in a ghost collision. Welding2.png
    So if ray hits triangle 1 we get the ghost collision on the blue normal. Problem is that if ray hits triangle 2 and we get the green normal, the sphere is actually already in a penetration of that normal and penetration recovery kicks in right away.

    I hope that this discourages everyone from using a raycast toward the contact point b on B :)

    Now something about the potential solution.

    As @petarmHavok proposed the collider should be casted and let's consider how that helps. If we think about case of avoiding ghost collision and having a hit we will end up with following thing:
    Welding3.png
    so we will cast body A from c to c'. The movement is just the velocity * dt. At the point of the callback the velocity of the body has already been adjusted by gravity effect in Unity.Physics. So cast hit will return the green normal and everything is fine.

    Lets check the second case with the same approach:
    Welding4.png
    We are doing the same thing just this time there is no hit. Since there is no hit we need to create some normal on our own. If we just disable the contact the triangle 2 may stay "unguarded" which may not be much of a problem on this diagram but in general it can be. So in this case we should form the green separation plane which is parallel to the movement vector. We shouldn't rotate the plane more than that because overshooting may result in body A ending up in the immediate penetration like on the second diagram.

    Important: Instead of doing the cast on the world level and ending up with the same normal for all contact points the collider cast has to be done on ((bodyA, colliderKeyA), (bodyB, colliderKeyB)) pair level. That is the only way to make sure that all original contacts are represented with proper ones after the cast.

    How to do this? For this we need to do something similar to what @mpforce1 did. Luckily there is no need to change the physics code. This time we get the leaf collider through collider.getLeaf(key, out targetLeaf) and then we use the result as a target for the cast and call targetLeaf.Collider->castCollider(). Unfortunately this is unsafe. I hope that you are find with it.

    Also, the input for castCollider has to be in the targetLeaf.Collider's space.


    I hope that this helps. I haven't written the code myself although we should definitely provide some referenced implementation. Welding is a very hard problem and there is no solution for it that fits all the cases. Havok has been building solution for welding for a decade and it's still not perfect, but still, using Havok has high probability of solving the welding problem for most of the games. I hope for those who cannot afford Havok this post gives enough information and reasoning around welding to try to implement solution that covers all their cases.
     
    GliderGuy and steveeHavok like this.
  16. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    WOW! This is a information treasure both for the proposed solution and for the pieces I've been missing in order to understand the root cause of the problem. Thanks a lot for the time and effort to write and draw this. I love the pictures and samples, reminds me of Project Anarchy documentation back in the days :)

    So, practically speaking, in order to check for the potential penetration, body A is moved along the "contact axes" so the point a and point b align, and then is performed "penetration check"? And the shared vertex is probably the reason from my observation about why the perpendicular edges result in so strong impulse. There is, however, only one thing left unclear and that is why we have 3 blue impulse vectors on the last picture I shared, why would we have more then one at any point in time, apart from when there is penetration? Those are vectors drawn by physics debug, btw.

    I'll fallow your proposal and keep you updated if I solve it or, potentially, hit the next bump. Thanks again!

    My aim is to build competitive mobile game with ball physics as it's core mechanics so that is why I chose Unity Physics, the promised cross-platform determinism.
     
  17. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    Each contact point of body A has corresponding contact point on body B but only contact points of body A are important. Solver treats bodyB as static and uses relative velocity of bodies as a body A velocity and then it just tries to prevent any contact point on body A to penetrate separating normal by applying impulse on each contact point that may go through separating plane.
    That process is applied on each separating plane. In your case this means between sphere and each triangle. Order of solving the pair (sphere-triangle) can be considered as arbitrary (but deterministic).

    To get back to your question. I am not sure how those impulses are created since they are drawn on contact points on traingles, not on a sphere. It looks like that rays are shot from the sphere position at the beginning of the frame and that screenshot is at the end of the frame when sphere didn't touch even a ghost plane (At least that's how i read the image).

    Maybe we shouldn't debug the current state before you move to new approach.
     
    KwahuNashoba likes this.
  18. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @Sima_Havok I'm having some trouble getting the collider cast to work.

    Code (CSharp):
    1.  
    2. private readonly struct Details {
    3.     public readonly float3 Position;
    4.     public readonly quaternion Rotation;
    5.     public readonly float3 Velocity;
    6.     public readonly ColliderKey Key;
    7.     public readonly BlobAssetReference<Collider> RootCollider;
    8.  
    9.     public Details(PhysicsWorld world, int index, ColliderKey key) {
    10.         var body = world.Bodies[index];
    11.         Position = body.WorldFromBody.pos;
    12.         Rotation = body.WorldFromBody.rot;
    13.         Velocity = world.GetLinearVelocity(index);
    14.         RootCollider = body.Collider;
    15.         Key = key;
    16.     }
    17. }
    18.  
    19. private unsafe bool TryFindSurfaceNormalByColliderCast(
    20.     ref ModifiableContactHeader contactHeader,
    21.     out float3 surfaceNormal
    22. ) {
    23.     var pair = contactHeader.BodyIndexPair;
    24.     var keys = contactHeader.ColliderKeys;
    25.     var a = new Details(PhysicsWorld, pair.BodyAIndex, keys.ColliderKeyA);
    26.     var b = new Details(PhysicsWorld, pair.BodyBIndex, keys.ColliderKeyB);
    27.     if (IsBallAndScenery(a, b, out var scenery, out var ball)) {
    28.         scenery.RootCollider.Value.GetLeaf(scenery.Key, out var sceneryLeaf);
    29.         ball.RootCollider.Value.GetLeaf(ball.Key, out var ballLeaf);
    30.         var inverseTargetRotation = math.inverse(scenery.Rotation);
    31.         var ballPositionInTargetSpace = math.mul(inverseTargetRotation, ball.Position);
    32.         var ballMovementInTargetSpace = math.mul(inverseTargetRotation, ball.Velocity * DeltaTime);
    33.         var ballRotationInTargetSpace = math.mul(inverseTargetRotation, ball.Rotation);
    34.         var endPosition = ballPositionInTargetSpace + ballMovementInTargetSpace;
    35.         if (sceneryLeaf.Collider->CastCollider(
    36.             new ColliderCastInput {
    37.                 Collider = ballLeaf.Collider,
    38.                 Start = ballPositionInTargetSpace,
    39.                 End = endPosition,
    40.                 Orientation = ballRotationInTargetSpace
    41.             }, out var hit
    42.         )) {
    43.             Debug.Log("Hit found");
    44.             surfaceNormal = hit.SurfaceNormal;
    45.             return true;
    46.         } else {
    47.             // TODO
    48.             surfaceNormal = default;
    49.             return false;
    50.         }
    51.     } else {
    52.         surfaceNormal = default;
    53.         return false;
    54.     }
    55. }
    56.  

    As you can see I'm trying to translate the ball position and rotation into the target's space but I'm not sure I'm doing it right because I never hit that debug log. Furthermore, the ball and scenery are both just in world space so translating them this way doesn't actually change the values. Additionally, I've found through debugging that I always hit this in Unity.Physics.ColliderCast.cs (line 165):

    Code (CSharp):
    1. // Check for a miss
    2. float dot = math.dot(distanceResult.NormalInA, input.Ray.Displacement);
    3. if (dot <= 0.0f)
    4. {
    5.   // Collider is moving away from the target, it will never hit
    6.   return false;
    7. }

    Which suggests that it thinks the ball is moving away from the target which isn't true, in this case it's resting on and thus falling towards the target due to gravity. Do you have any idea what I'm doing wrong here?
     
  19. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    The problem is in your collider cast - you did transform the start and end to target space, but only accounted for rotation. There is also a translation part of it. So you need to do a full transform, not just rotate.

    You can take a look at RigidBody.CastCollider(), that's exactly what happens there (in latest Unity Physics package, previously it was a bit more hidden).
     
  20. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @petarmHavok Thanks for responding. Are you referring to an unreleased version of the physics package? I think I'm running with the latest version (0.3.2-preview) and all I can see is this (line 61):

    Code (CSharp):
    1.  
    2. public bool CastCollider(ColliderCastInput input) => QueryWrappers.ColliderCast(ref this, input);
    3. public bool CastCollider(ColliderCastInput input, out ColliderCastHit closestHit) => QueryWrappers.ColliderCast(ref this, input, out closestHit);
    4. public bool CastCollider(ColliderCastInput input, ref NativeList<ColliderCastHit> allHits) => QueryWrappers.ColliderCast(ref this, input, ref allHits);
    5. public bool CastCollider<T>(ColliderCastInput input, ref T collector) where T : struct, ICollector<ColliderCastHit>
    6. {
    7.     return Collider.IsCreated && Collider.Value.CastCollider(input, ref collector);
    8. }
    9.  
    At any rate, is the solution to simply subtract the scenery.Position from ballPositionInTargetSpace?
     
  21. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Let me share a bit of "progress" I made.

    Based on what @Sima_Havok proposed, I did next steps for each contact header:
    1. Get leaf of static collider (it returns triangle, so OK)
    2. Get WorldFromBody of dynamic collider and convert it to static collider leafs local space
    3. Get dynamic body velocity vector multiplied by delta time and convert it to local space
    4. Use this to create start and end point for cast
    5. Cast sphere collider against collider leaf
    6. If there is a hit, set hit surface normal as new contact header normal (this should cover the first case from Simas examples)
    Now the results. Hit surface normals are represented in cyan, linear velocity motion (the line I'm sweeping collider on) is tiny white line and the rest is Unity Physics debug.

    Frame before the edge get hit (single normal)
    upload_2020-6-18_0-42-26.png

    Frame when the edge get hit (two normals)
    upload_2020-6-18_0-42-57.png

    Frame after the edge got hit (no normals, ball is in the air)
    upload_2020-6-18_0-43-51.png

    Here are my observations:
    1. Hit points/normals are approximately the same as what we get from contact header or as impulse from collision event (blue and green lines)
    2. If I multiply velocity vector by 200, a get significantly less bumpiness. Which might be completely random result i.e. affected by the angle of the slope
    Now here is what is strange and is introducing confusion to understanding problem:
    1. Collider cast is done based on rigid body position and linear velocity so all hits should be at the same place. If we look a the picture, not all green arrows have companion normal. This can be explained by the fact that al normals are collinear and we see multiple as single one. And occurrence of more than one normal could be floating point error???
    2. This is sort of related to the previous one. Not all casts in single frame hit something. For the matter of fact, there are more missed ones. This can also explain green arrows without company of cyan line upload_2020-6-18_1-24-28.png
     
    Last edited: Jun 18, 2020
    GliderGuy likes this.
  22. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    Ah, pardon me, not the first time I've done this. :) Yes, it's unreleased code..

    Anyway, check https://forum.unity.com/threads/distance-query-not-working.911171/#post-5993369, it contains a proper moving into collider local space. It should get you started.
     
    GliderGuy and KwahuNashoba like this.
  23. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    OK, one last question before I make a decision to bail this and use Havok backend until someone else makes this work. Either I'm doing something wrong, or physics debug display is not working OK.

    Please @Sima_Havok try to answer this. Based on the example below, the things that you wrote about calculating impulses and separation normals does not seam to get along with what I can observe from the picture bellow.

    So what I did is I created simple scene with angled flat slope and assigned mesh collider. That slope is previewed as green line, observed from the left side. The contact at the bottom of the image is an edge contact. Black line is old normal and green line is corrected normal, which is basically the normal of the velocity motion vector (black line originating from sphere center). This is actually my try on case 2 from your examples, when collider did not hit anything. So, based on what you explained, at this point, sphere should be in the penetration and penetration recovery should kick the ball, but nothing really happens. This is introducing a lot of confusion while I'm trying to debug my logic.


    upload_2020-6-27_20-12-36.png

    Also, if I try to set distance on contact point for corrected normal to 0, ta sphere climbs on new normals like so:
    upload_2020-6-27_20-38-5.png

    Now there are no green arrows because their length is, obviously, zero. My only explanation of this behavior is that there is "matching" contact point on B with distance from b to a, so if I set distance a to b to be 0, then relative distance becomes b to a.

    Again, it looks like I'm still not understanding the root cause of the problem, despite your explanation. Is it normal, distance, contact position or any combination of them that I should correct in order to fix this.
     
    GliderGuy likes this.
  24. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @petarmHavok Sorry I haven't been able to get back for a while. Thanks for your help, I eventually think I managed to get a working cast, the code you mentioned was released soon after which gave me a better understanding.

    With regards to the second case @Sima_Havok outlined, I think I've managed to implement it as suggested. However, the ball is still getting nudged slightly when it crosses a mesh edge. This still represents major progress, it's no longer getting sent flying into the air for example, but I'd still like to refine the solution even more if possible. Please could you check I'm doing the correct things here?

    Code (CSharp):
    1.  
    2. private static Math.MTransform Inverse(Math.MTransform a) {
    3.     var inverseRotation = math.transpose(a.Rotation);
    4.     return new Math.MTransform {
    5.         Rotation = inverseRotation, Translation = math.mul(inverseRotation, -a.Translation)
    6.     };
    7. }
    8.  
    9. private static float3 Mul(Math.MTransform a, float3 x) => math.mul(a.Rotation, x) + a.Translation;
    10.  
    11. private unsafe bool TryFindSurfaceNormalByColliderCast(
    12.     ref ModifiableContactHeader contactHeader,
    13.     ref ModifiableContactPoint contactPoint,
    14.     out float3 surfaceNormal
    15. ) {
    16.     var a = new Details(PhysicsWorld, contactHeader.BodyIndexA, contactHeader.ColliderKeyA);
    17.     var b = new Details(PhysicsWorld, contactHeader.BodyIndexB, contactHeader.ColliderKeyB);
    18.     if (IsBallAndScenery(a, b, out var scenery, out var ball)) {
    19.         scenery.RootCollider.Value.GetLeaf(scenery.Key, out var sceneryLeaf);
    20.         ball.RootCollider.Value.GetLeaf(ball.Key, out var ballLeaf);
    21.  
    22.         // Transform the ball into target space
    23.         var targetWorldFromBody = new Math.MTransform(scenery.WorldFromBody);
    24.         var targetBodyFromWorld = Inverse(targetWorldFromBody);
    25.  
    26.         var ballRotationInTargetSpace = math.mul(math.inverse(scenery.WorldFromBody.rot), ball.WorldFromBody.rot);
    27.         var ballPositionInTargetSpace = Mul(targetBodyFromWorld, ball.WorldFromBody.pos);
    28.         var ballMovementInTargetSpace = math.mul(targetBodyFromWorld.Rotation, ball.Velocity) * DeltaTime;
    29.  
    30.         var endPosition = ballPositionInTargetSpace + ballMovementInTargetSpace;
    31.         if (sceneryLeaf.Collider->CastCollider(
    32.             new ColliderCastInput {
    33.                 Collider = ballLeaf.Collider,
    34.                 Start = ballPositionInTargetSpace,
    35.                 End = endPosition,
    36.                 Orientation = ballRotationInTargetSpace
    37.             }, out var hit
    38.         )) {
    39.             surfaceNormal = math.mul(scenery.WorldFromBody.rot, hit.SurfaceNormal);
    40.             return true;
    41.         } else {
    42.             var contactPointInTargetSpace = Mul(targetBodyFromWorld, contactPoint.Position);
    43.             var contactToBallStart = contactPointInTargetSpace - ballPositionInTargetSpace;
    44.             surfaceNormal = ballPositionInTargetSpace +
    45.                 Project(contactToBallStart, ballMovementInTargetSpace) -
    46.                 contactPointInTargetSpace;
    47.             surfaceNormal = math.mul(scenery.WorldFromBody.rot, surfaceNormal);
    48.             return true;
    49.         }
    50.     } else {
    51.         surfaceNormal = default;
    52.         return false;
    53.     }
    54. }
    55.  
    56. private static float3 Project(float3 vector, float3 onNormal) {
    57.     var num = math.dot(onNormal, onNormal);
    58.     if (num < 1.401298E-45f) {
    59.         return float3.zero;
    60.     } else {
    61.         return onNormal * math.dot(vector, onNormal) / num;
    62.     }
    63. }

    My main concern is that I'm not sure what space the resultant normal and input contact point position should be/are in. You can see I'm assuming they're both in world space. Additionally, I'm using the formula outlined here to figure out the normal of the "green separation plane" mentioned in part 2 of the solution. However, I don't know if the magnitude of this normal makes sense for what we're trying to do. Finally, the resultant normal is used like this:

    Code (CSharp):
    1.  
    2. public void Execute(ref ModifiableContactHeader contactHeader, ref ModifiableContactPoint contactPoint) {
    3.     if (TryFindSurfaceNormalByColliderCast(
    4.         ref contactHeader,
    5.         ref contactPoint,
    6.         out var surfaceNormal
    7.     )) {
    8.         if (contactPoint.Index == 0) {
    9.             var newNormal = surfaceNormal;
    10.             distanceScale = math.dot(newNormal, contactHeader.Normal);
    11.             contactHeader.Normal = newNormal;
    12.         }
    13.  
    14.         contactPoint.Distance *= distanceScale;
    15.     }
    16. }
    17.  

    This is how I used the new surface normal in my previous attempt where I used the normal of the face of the triangle that was hit, it's based on the original ModifyNormalsJob in the unity ecs samples. I hope this usage still applies in this context.

    Again, thank you so much for helping us solve this issue.
     
  25. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    Hey @mpforce1, as you have noticed I wasn't present on the forum this week :(

    Let's start with the first post I didn't reply:
    In the case I draw the mesh was not flat so overshoot could happen. In the case of flat mesh it cannot happen. So what you did is that you actually ended up with two identical normals, not just by direction but by their position. So there was no penetration and no penetration recovery

    Here things depend on the order. If you first set distance to 0 then original separating plane is touching the sphere, and if you rotate separating plane in any direction around the contact point then the sphere will penetrate that new separating plane.
     
  26. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    @mpforce1, for the TryFindSurfaceNormalByColliderCast, when you calculate normal orthogonal to the sphere movement you definitely have to normalize it.

    Assumption that everything should be in world space is correct.

    Regarding the scaling in the second snippet, it should take into account the sphere radius
    Situations is that you have a sphere and red triangle, you want to get separating normal which is represented as dotted red line. Blue arrow is going from contact point on triangle to contact point on sphere and its direction is initial normal and length is distance
    DistanceCalc0.png
    The scaling code from your snippet projects the blue vector to new normal which is equal to triangle normal and give you the distance represented with that blue "arrow" shape. Which is obviously wrong.
    DistanceCalc1.png
    What you need to do is to increase length of blue vector by sphere radius to get green arrow, then project that arrow to new normal and shorten it by sphere radius. That would give you the distance represented by a yellow arrow
    DistanceCalc2.png

    I hope this will improve the behavior
     
  27. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @Sima_Havok Thanks for your follow up. I've tried to follow your suggestions, here's the updated code:
    Code (CSharp):
    1. private struct ModifyNormalsJob : IContactsJob {
    2. [ReadOnly]
    3. public PhysicsWorld PhysicsWorld;
    4.  
    5. public float DeltaTime;
    6.  
    7. private float3 originalContactNormal;
    8.  
    9. public void Execute(ref ModifiableContactHeader contactHeader, ref ModifiableContactPoint contactPoint) {
    10.     if (contactPoint.Index == 0) {
    11.         originalContactNormal = contactHeader.Normal;
    12.     }
    13.     if (TryFindContactNormal(
    14.         ref contactHeader,
    15.         ref contactPoint,
    16.         out var contactNormal
    17.     )) {
    18.         if (contactPoint.Index == 0) {
    19.             contactHeader.Normal = contactNormal;
    20.         }
    21.     }
    22. }
    23.  
    24. private readonly struct Details {
    25.     public readonly RigidTransform WorldFromBody;
    26.     public readonly float3 Velocity;
    27.     public readonly ColliderKey Key;
    28.     public readonly BlobAssetReference<Collider> RootCollider;
    29.  
    30.     public Details(
    31.         PhysicsWorld world,
    32.         int index,
    33.         ColliderKey key
    34.     ) {
    35.         var body = world.Bodies[index];
    36.         WorldFromBody = body.WorldFromBody;
    37.         Velocity = world.GetLinearVelocity(index);
    38.         RootCollider = body.Collider;
    39.         Key = key;
    40.     }
    41. }
    42.  
    43. private unsafe bool TryFindContactNormal(
    44.     ref ModifiableContactHeader contactHeader,
    45.     ref ModifiableContactPoint contactPoint,
    46.     out float3 contactNormal
    47. ) {
    48.     var a = new Details(PhysicsWorld, contactHeader.BodyIndexA, contactHeader.ColliderKeyA);
    49.     var b = new Details(PhysicsWorld, contactHeader.BodyIndexB, contactHeader.ColliderKeyB);
    50.     if (IsBallAndScenery(a, b, out var scenery, out var ball)) {
    51.         scenery.RootCollider.Value.GetLeaf(scenery.Key, out var sceneryLeaf);
    52.         ball.RootCollider.Value.GetLeaf(ball.Key, out var ballLeaf);
    53.  
    54.         // TODO calculate from more collider types
    55.         float shapeRadius;
    56.         if (ballLeaf.Collider->Type == ColliderType.Sphere) {
    57.             shapeRadius = ((SphereCollider*) ballLeaf.Collider)->Radius;
    58.         } else {
    59.             shapeRadius = 0.05f;
    60.         }
    61.  
    62.         var contactPointToBallPosition = ball.WorldFromBody.pos - contactPoint.Position;
    63.  
    64.         // Transform the ball into target space
    65.         var targetWorldFromBody = new Math.MTransform(scenery.WorldFromBody);
    66.         var targetBodyFromWorld = Inverse(targetWorldFromBody);
    67.  
    68.         var ballRotationInTargetSpace = math.mul(math.inverse(scenery.WorldFromBody.rot), ball.WorldFromBody.rot);
    69.         var ballPositionInTargetSpace = Mul(targetBodyFromWorld, ball.WorldFromBody.pos);
    70.         var ballMovementInTargetSpace = math.mul(targetBodyFromWorld.Rotation, ball.Velocity) * DeltaTime;
    71.  
    72.         var endPosition = ballPositionInTargetSpace + ballMovementInTargetSpace;
    73.         if (sceneryLeaf.Collider->CastCollider(
    74.             new ColliderCastInput {
    75.                 Collider = ballLeaf.Collider,
    76.                 Start = ballPositionInTargetSpace,
    77.                 End = endPosition,
    78.                 Orientation = ballRotationInTargetSpace
    79.             }, out var hit
    80.         )) {
    81.             contactNormal = math.mul(scenery.WorldFromBody.rot, hit.SurfaceNormal);
    82.  
    83.             var distanceScale = math.dot(contactNormal, originalContactNormal);
    84.             contactPoint.Distance *= distanceScale;
    85.  
    86.             return true;
    87.         } else {
    88.             // Construct the normal to the plane parallel to the direction of movement
    89.             // https://answers.unity.com/questions/535822/how-to-find-the-vector-that-passes-through-a-point.html
    90.             var contactPointInTargetSpace = Mul(targetBodyFromWorld, contactPoint.Position);
    91.             var contactToBallStart = contactPointInTargetSpace - ballPositionInTargetSpace;
    92.             contactNormal = ballPositionInTargetSpace +
    93.                 Project(contactToBallStart, ballMovementInTargetSpace) -
    94.                 contactPointInTargetSpace;
    95.             contactNormal = math.mul(scenery.WorldFromBody.rot, contactNormal);
    96.             contactNormal = math.normalizesafe(contactNormal);
    97.  
    98.             // Project the vector from the contact point to the center of the shape to the new new normal,
    99.             // then subtract the shape's radius
    100.             var projected = math.dot(contactPointToBallPosition, contactNormal) / math.length(contactNormal);
    101.             contactPoint.Distance = projected - shapeRadius;
    102.             return true;
    103.         }
    104.     } else {
    105.         contactNormal = default;
    106.         return false;
    107.     }
    108. }

    If I understood correctly, that final bit implements what you suggested about projecting the green arrow to the new normal then subtracting the ball radius. I believe contactPointToBallPosition should represent the green vector although I'm not certain. I'm also not certain if the contactPoint.Distance should be set to that value or multiplied by it.

    I was a bit uncertain about whether what I was originally doing with the distance scale was still valid for part 1 of the solution, where the collidercast hits, so I've left that essentially as it was.

    However, even after all of this I'm still seeing the balls getting nudged by the mesh edges. Any further advice would be appreciated.
     
  28. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    contactPoint.Position is position on body A not on bodyB so contactPointToBallPosition, contactPointInTargetSpace, contactToBallStart and maybe others are wrong

    In all formulas contact point on B should be used:
    contactPointOnB = contactPoint.Position - contactHeader.Normal * contactPoint.Distance
     
  29. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @Sima_Havok Sorry if this is obvious but, is entity A in the contact header always the ball and entity B always the scenery? I can't see anything in the docs that gives an explicit order.
     
  30. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    In the pair where one body is static and one dynamic, body A is always dynamic
     
    florianhanke likes this.
  31. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    Also, normal always goes from body B to body A so when you use debug visualization you know which one is A and which one is B
     
    florianhanke likes this.
  32. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @Sima_Havok Hi again, it looks like it's the other way around. ContactPoint.Position is the position on body B rather than body A. I had a go at drawing the vectors and found that if I did your calculation to get the contact point on body b it always starts below the actual contact point on the collider edge.

    As far as I can tell, the other things you mentioned are correct. The normal does seem to go from B to A and body A is always the ball and B the scenery.

    I'm going to have a go at drawing some more of the calculation steps to see if they are where I expect them to be, in particular the resultant contact normal doesn't seem to be in line with the perpendicular to the direction of movement so I must have messed something up there.
     
    Sima_Havok likes this.
  33. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    To follow up, it looks like I've made a mistake in generating the separating plane. I had assumed that all I needed to do was produce a normal vector from a plane that was parallel to the direction of movement and passed through the contact point. However, rechecking the diagram, I can see that the plane may actually need to pass through the entire edge and thus be parallel to that edge. Is this correct? I'm a bit apprehensive about this because this system doesn't distinguish between edge and face contacts, although it would be nice if it could, meaning there might not be an edge associated with the contact.

    If this is correct, how can I go about getting this edge data without modifying the physics package? I assume I would have to use ConvexHull.GetSupportingFace or something similar but you mentioned that we shouldn't have to modify the package. If not, could you please point me in the right direction?
     
  34. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    This is when things get complicated and I haven't elaborated enough on that. Main purpose of the normal is to define separating plane which hides whole triangle. So if you had an edge collision there are multiple possibilities.
    (all diagrams below have two triangles drawn but we are discussing altering normal of the right one. Blue line is trajectory of the sphere)

    1. Triangle normal is enough
    On the image below you can see that triangle normal is enough. So if cast doesn't provide hit and if sphere with its movement doesn't breach the triangle normal then you don't need to change the contact points because that plane contains the edge.
    RotatingUpToTriangleNormal.png

    2. Object breaches triangle normal
    If cast doesn't provide the hit but sphere breaches the triangle normal then you need to rotate normal over the triangle normal but then you can't keep your contact points. So once you find the plane which is orthogonal to the object movement you need to position contact point so that normal that separation plane that goes through that normal needs to have whole triangle "below" it. So most likely you will have the single contact point in triangle vertex. Since you can't remove the contact point you should replace both original contact points with single new one.
    RotatingOverTriangleNormal.png

    3. Object penetrates triangle normal in starting position
    There are cases when object is not hitting the triangle normal because it is moving away from it but it penetrates triangle normal in starting position. So you can't set normal to it because penetration recovery will kick in. So then you need to find the normal as in the case 2 and position it to one of the vertices of the edge that original normal was on.
    ObjectPenetrates triangle normal.png
    So all in all in some cases you can keep original contact point(s) (case 1). In some cases you will keep contact point on the edge where it original was (3) and on some cases you will move contact point out of original edge.

    Those cases consider options when starting normal is edge or vertex normal but similar cases exist when starting normal is triangle normal (consider left triangle in third case, you will end up with the same normal as for the right triangle (in 2d case).

    Main issue with this whole problem space is that 2d cases that we usually draw don't paint the full picture and don't illustrate all the problems.
     
  35. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    @mpforce1, you were right about ContactPoint.Position. I relied on the comment but comment is completely wrong
     
  36. mpforce1

    mpforce1

    Joined:
    Apr 4, 2014
    Posts:
    34
    @Sima_Havok Hi again, I have some questions about what you posted.
    What do you mean here by "breach triangle normal"? If the collider cast doesn't provide a hit then the sphere cannot have breached the triangle geometry otherwise we would have had a hit. My next guess would be that you mean "intersects the plane described by the contact point and the triangle normal", is this a correct reading?
    I'm having trouble understanding what you mean in this paragraph. Are you saying that the sphere intersects the plane described by the contact point and the triangle normal at some point along its movement?
    You mention using a triangle vertex as the contact point. My intuition is that the closest point on the triangle to the movement is the best value for this, if this is wrong then how would I find the appropriate point?
    Furthermore, the final sentence says that I should "replace both original contact points with a single new one", but, at least with a sphere collider, I thought we only ever had 1 contact point.
    In this case are you saying that the thing that differentiates it from the 2nd case is that the sphere is already intersecting the plane described by the contact point and the triangle normal without taking into account its movement?

    Something else I'm unsure about here is the ContactPoint.Distance, does this need to be changed, particularly in the case where we change the ContactPoint.Position, it seems like it should?

    Thanks for your patience.
     
  37. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    That is true, but normal defines a plan which is in infinite and sphere can hit that plan outside of the original triangle that plane fully contains. Breaching of the plane outside of the triangle is my second diagram.

    Yes it intersects both.
    If you look at the second image you can see that new normal which is parallel to the movement. If that plane is in original contact point that would mean that all the triangle is sticking out that new normal and that sphere could tunelling if other object changes its velocity during the single step. For that reason that new separating plane is lifted "up" so that whole triangle is below it. In the second image it contains the vertex that is opposite of the edge that contained original contact point.
    You are correct, in your case you always have only one contact point.

    Correct. If you draw the plane that contains right triangle sphere would be penatrating it.

     
  38. Sima_Havok

    Sima_Havok

    Joined:
    Dec 16, 2019
    Posts:
    47
    It is obvious that implementing this is very complicated with and there are many cases to be handled.

    With that being said we should provide some reference implementation in a demo and people can adjust it if they need to. Two things are important:
    • Solution will not be either perfect or fast but it will cover lots of cases. All approaches like this are heuristics because there is no way to predict everything and solve all possible cases because lots of things can happen during solve that break assumptions used when normals were altered.
    • Timeline for this reference implementation is unknown at the moment but I will try to raise its priority during the next planning session.
     
    GliderGuy and mpforce1 like this.
  39. tomfulghum

    tomfulghum

    Joined:
    May 8, 2017
    Posts:
    1
    Sorry for necroing, but I'm facing the same problems outlined in this thread and was wondering if there's an update on this?
     
  40. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    91
    Not on my side. I've abandoned the project for almost a year. Recently I put my motivation together in other to give it another try with use cases that Sima presented in his long post, but with no luck after all. Maybe I can clean up the project and put it on git, but not sure how much usefull info is in it.
     
unityunity