Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Have a look at our Games Focus blog post series which will show what Unity is doing for all game developers – now, next year, and in the future.
    Dismiss Notice

Experimental contacts modification API

Discussion in 'Physics Previews' started by yant, Jul 3, 2020.

  1. XANTOMEN

    XANTOMEN

    Joined:
    Dec 18, 2012
    Posts:
    41
    [EDIT: Solved my problems, so this can be ignored now. I'm still curious about how the order of object initialization works and why it changes when changing Layers, but it would not benefit my current usecase anyway. Posting some code on my own for reference to other people]

    I stumbled upon this thread a few days ago by accident and I'm so impressed with this feature, I love it! Props to @yant and also the other people in this thread making cool stuff with it. Thank you very much for your efforts!

    Unfortunately, I'm entering some brain pickles in my way to try and do what I want with it, and I wanted to gather your (@yant or anyone that knows) thoughts on it.

    [EDIT: Ended up not needing to alter the point, as I had misunderstood what I needed, so these point became irrelevant for my usecase]
    1. I have read this bug report and the response to it, both in the bug report and in the thread. Namely:

    Could someone clarify if there's a way to force such "object initialisation" to be in a predictable order? I don't mind about if the object I want to check for comes as "collider" or "otherCollider", I can deal with that easy. However, the fact that the world pos of the point (getting it via GetPoint(i) is different in both cases and I can't predict if I'm going to get the value I want or a different one is making things quite messy, and I don't like not knowing where the order is coming from. For example, if I have two objects in place, and I change one of them to a layer (by clicking in the Editor) where it does not collide with the other one, then bring it back to the original layer, the order of those colliders (and therefore where the point in GetPoint(i) ends up) is now different. I can repeat this double switch many times and it will keep changing which one is the "otherCollider" and which one is the first.

    Gifs of the result before and after doing the double layer switch:

    Desired case


    "Wrong" case


    The debug ray I'm doing with this code:

    Code (CSharp):
    1. var arrow = pair.GetNormal(i) * pair.GetSeparation(i) * Mathf.PingPong(time * speedMultiplier, 1f);
    2.  
    3. Debug.DrawRay(pair.GetPoint(i), arrow, color);
    Luckily, my one neuron made me realize that via hashing them both in different hashmaps I can easily tell when I'm getting the small sphere as a first collider or "otherCollider" and when in the undesired order, change its point, normal and separation accordingly to where my desired contact point would have been by changing the "wrong" point value to "wrong" point + "wrong" normal * "wrong" separation, but I'm not sure if other vars are harder to invert and I'm missing something, and in any case, if I could, I would rather do whatever necessary to make the order predictable from the get go. (or at the very least, understand under which circumstances the order might change, so I can potentially think of something)

    Notes: Both spheres I used for the gifs have kinematic RigidBody with a child, and that child has a Convex Mesh Collider of a Sphere. (I have my reasons to be testing with this instead of Sphere Colliders). Only the big Sphere has "hasModifiableContacts" set to true.

    [EDIT: Reference code of the solution I implemented at the end of the post]
    2. In the following image,



    I need to tell if each contact point between the two Spheres (they are Spheres in this example but I need a solution that works for any convex shape) is also inside the Monkey hull or outside, the three of them having a convex collider. My best idea so far was to loop through the contact points of both Spheres and pass them through the ClosestPoint() function of the Monkey collider (hashed previously), which it being convex, would return the same point I fed the function, giving me my answer.

    However, this function can only be used in the main thread, so that's a no go.

    My next best idea is to hash all the transformed (world pos) vertices of the Monkey collider beforehand (I do have a VHACD convex version of the mesh that lines up with the collider perfectly, so I'm taking the vertices from there, altho I would not mind if someone explained how to get the vertices of Unity's native mesh collider convex hull), and during the modification event, wrap my head around this clever technique and implement it to tell if each of my contact points are inside the Monkey convex hull or not.

    Am I being crazy here, and there's an easier way to do this check inside the modification event, so I can ignore the contact points I need to and let the others pass?

    [EDIT] I did try the approach from a comment in the Stack Overflow post I linked (the one of adding all vectors together, then using that summed vector as a reference and compare by angle). It looked really cool when debugging it with gizmo lines, but it did not really work, as having more vertices in one side of the mesh would bias the summed vector in that direction, and some vectors could end up "behind" the imaginary plane, giving wrong positives for "is inside volume".

    Instead, I ended up using the following code. Look up the initial code comment and the links in it to find the inspiration for the approach if unclear:

    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. //Tried this approach before but it was slightly off when vectors were repeating more on one side of the volume
    6. //https://stackoverflow.com/questions/4901959/find-if-a-point-is-inside-a-convex-hull-for-a-set-of-points-without-computing-th/4903615#4903615
    7. //Ended up implementing more or less this one: https://stackoverflow.com/a/16906278 , leveraging Plane.GetSide() function, but could as well have used
    8. //an angle > 90 comparison (if vertex to targetPoint vector angle with normal is > 90 then targetPoint is on the left of the face plane)
    9.  
    10.  
    11. public class PointInsideConvexCollidersTest : MonoBehaviour
    12. {
    13.     public Vector3 testPoint;
    14.  
    15.     public MeshCollider[] meshColliders;
    16.  
    17.     public float sizeTargetSphereGizmo = 0.2f;
    18.  
    19.     public Dictionary<MeshCollider, Plane[]> transformedFacePlanesByMeshCollider = new Dictionary<MeshCollider, Plane[]>();
    20.  
    21.     bool isInside;
    22.  
    23.     public void OnDrawGizmos()
    24.     {
    25.         if (isInside)
    26.         {
    27.             Gizmos.color = Color.cyan;
    28.         }
    29.         else
    30.         {
    31.             Gizmos.color = Color.red;
    32.         }
    33.  
    34.         Gizmos.DrawWireSphere(testPoint, sizeTargetSphereGizmo);
    35.     }
    36.  
    37.     private void OnEnable()
    38.     {
    39.         meshColliders = GetComponentsInChildren<MeshCollider>();
    40.         UpdateTransformedFacePlanes();
    41.     }
    42.  
    43.     private void FixedUpdate()
    44.     {
    45.         if (transform.hasChanged)
    46.         {
    47.             UpdateTransformedFacePlanes();
    48.             transform.hasChanged = false;
    49.         }
    50.  
    51.         isInside = IsPointInsideConvexMeshColliders(meshColliders, testPoint);
    52.     }
    53.  
    54.     void UpdateTransformedFacePlanes()
    55.     {
    56.         transformedFacePlanesByMeshCollider.Clear();
    57.  
    58.         foreach (var meshCollider in meshColliders)
    59.         {
    60.             List<Plane> facePlanes = new List<Plane>();
    61.  
    62.             if (meshCollider.sharedMesh.normals.Length == 0)
    63.                 meshCollider.sharedMesh.RecalculateNormals();
    64.  
    65.             var triangleIndices = meshCollider.sharedMesh.triangles;
    66.             var vertices = meshCollider.sharedMesh.vertices;
    67.             var normals = meshCollider.sharedMesh.normals;
    68.  
    69.             //Step through for loop 3 by 3
    70.             for (var i = 0; i < triangleIndices.Length; i+=3)
    71.             {
    72.                 var p1 = vertices[triangleIndices[i]];
    73.                 var p2 = vertices[triangleIndices[i + 1]];
    74.                 var p3 = vertices[triangleIndices[i + 2]];
    75.  
    76.                 var pCenter = (p1 + p2 + p3) / 3f;
    77.  
    78.                 var n1 = normals[triangleIndices[i]];
    79.                 var n2 = normals[triangleIndices[i + 1]];
    80.                 var n3 = normals[triangleIndices[i + 2]];
    81.  
    82.                 var normal = (n1 + n2 + n3) / 3f;
    83.  
    84.                 //Debug.DrawRay(trP1, trN1.normalized, Color.red);
    85.                 //Debug.DrawRay(trP2, trN2.normalized, Color.yellow);
    86.                 //Debug.DrawRay(trP3, trN3.normalized, Color.cyan);
    87.  
    88.                 var transformedVertex = meshCollider.transform.TransformPoint(pCenter);
    89.                 var transformedNormal = meshCollider.transform.TransformVector(normal);
    90.  
    91.                 //Debug.DrawRay(transformedVertex, transformedNormal.normalized, Color.yellow);
    92.  
    93.                 Plane facePlane = new Plane(transformedNormal, transformedVertex);
    94.  
    95.                 facePlanes.Add(facePlane);
    96.             }
    97.  
    98.             transformedFacePlanesByMeshCollider[meshCollider] = facePlanes.ToArray();
    99.         }
    100.     }
    101.  
    102.     public bool IsPointInsideConvexMeshColliders(MeshCollider[] meshColliders, Vector3 targetPoint)
    103.     {
    104.         foreach (var meshCollider in meshColliders)
    105.         {
    106.             if (IsPointInsideConvexMeshCollider(meshCollider, targetPoint))
    107.             {
    108.                 return true;
    109.             }
    110.         }
    111.  
    112.         return false;
    113.     }
    114.  
    115.     public bool IsPointInsideConvexMeshCollider(MeshCollider meshCollider, Vector3 targetPoint)
    116.     {
    117.         var facePlanes = transformedFacePlanesByMeshCollider[meshCollider];
    118.  
    119.         foreach (var facePlane in facePlanes)
    120.         {
    121.             //Keep in mind, in this case, GetSide() will evaluate true for the range (0,infinite]. So, 0 is treated as Inside.
    122.             //Inverted the normal of the planes if it was meant to treat 0 as Outside.
    123.             if (facePlane.GetSide(targetPoint))
    124.                 return false;
    125.         }
    126.  
    127.         return true;
    128.     }
    129. }
    130.  
    Thanks a lot and keep doing great work!

    [EDIT: This is a sneak peek into what I'm doing with it. I will be releasing a cutting asset of any shape versus any shape through Fragment Shaders that includes accurate collisions and shadows in the near future. The balls could have been any shape as well and it would work, as the code is agnostic to any shape of Cutters, CutTargets or physics objects]

     
    Last edited: Oct 6, 2022
    JuozasK, Edy and mgear like this.
  2. iDerp69

    iDerp69

    Joined:
    Oct 27, 2018
    Posts:
    34
    I don't understand this at all. I use the contact mods API in the context of a fully predicted/rollback Photon Fusion sim. I was getting a misprediction around ignoring all contacts in a pair, where sometimes the contact pair wouldn't be ignored. The apparent "solution" was to adjust my function:
    Code (CSharp):
    1. void IgnoreContactPair(ModifiableContactPair pair)
    2. {
    3.     for (int i = 0; i < pair.contactCount + 2; ++i)
    4.         pair.IgnoreContact(i);
    5. }
    Yes... +2.
    Makes no sense to me, and yet completely solves the issue I was having. What could the reason be that there would be unaccounted contacts in the pair?
     
    Last edited: Nov 3, 2022
  3. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,085
    Hmm, in our game we dampen the collision forces, but sometimes - rarely but sometimes - our collider hits a wall and bounces away with great force. I wonder if this is the culprit..?
     
  4. codebiscuits

    codebiscuits

    Joined:
    Jun 16, 2020
    Posts:
    13
    Is there somewhere I can see this bug? Sorry if it's obvious, I can't seem to find it by Googling.

    SetMaxImpulse seems to be working for me for Rigidbody, however, the bug still seems to apply for me to ArticulationBody.

    Or, @yant Should SetMaxImpulse even work for ArticulationBody? Maybe that's not the intent, or maybe I'm using it wrong.

    Edit: Using Unity 2021.3.5f1 LTS. I can provide a tiny project that demonstrates this if that's helpful.

    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Collections;
    3.  
    4. public class ContactModificationTest : MonoBehaviour {
    5.  
    6.     //Set to 0, both Rigidbody and ArticulationBody will immediately fall through a test plane;
    7.     //Set to 1e-07, Rigidbody will fall through test plane, AritculationBody travels along plane as usual
    8.     //Set to 0.5, Rigidbody will "skate through" bumps<3; ArticulationBody bangs over them as usual
    9.     public float maxImpulse = .5f;
    10.  
    11.     void OnEnable() {
    12.         var colliders = GetComponentsInChildren<Collider>();
    13.         foreach ( var c in colliders )
    14.             c.hasModifiableContacts = true;
    15.         Physics.ContactModifyEvent += ModificationEvent;
    16.     }
    17.  
    18.     private void ModificationEvent( PhysicsScene scene, NativeArray<ModifiableContactPair> pairs ) {
    19.         foreach ( var pair in pairs ) {
    20.             for ( int i = 0; i < pair.contactCount; ++i )
    21.                 pair.SetMaxImpulse( i, maxImpulse );
    22.         }
    23.     }
    24.  
    25. }
    26.  
    What I'm actually trying to achieve is a squashy tyre effect like this: https://twitter.com/ArturBerk/status/1586397732088209410
    (I think that video is using Havok, but anyway, I'm hoping I can manage with Unity/PhysX:)
     
    Last edited: Nov 15, 2022
  5. JuozasK

    JuozasK

    Unity Technologies

    Joined:
    Mar 23, 2017
    Posts:
    72
    That was a case number in a system where we tracked incoming bugs. You would normally be able to find a case's public page here https://issuetracker.unity3d.com/ by searching for that number.

    In this specific case (no pun intended), it was never converted to a bug, so the issue tracker page was not generated, so that's why it won't show up there.

    Interesting to see that it differs, I would be under the impression that it should work for articulations too. Will investigate. Thanks for the repro script :)

    If you could report a bug and post the case number that you get from our lovely support staff, we could take a look at the project and see if it's actually a bug.
     
    Last edited: Nov 16, 2022
    codebiscuits likes this.
  6. codebiscuits

    codebiscuits

    Joined:
    Jun 16, 2020
    Posts:
    13
    Hi,
    I got a mail saying CASE IN-23249, but it told me not to post a link (although, I can actually post a link if that's helpful, I only attached a proof-of-concept project).

    I attached the project here too if that's helpful (minus the library etc folder, so only 59k).

    Behavior is the same in 2021.3.13f1 (latest LTS), and also in 2022.1.23f1.

    Apologies in advance if the bug is, in the end, me.
     

    Attached Files:

    yant and JuozasK like this.
  7. JuozasK

    JuozasK

    Unity Technologies

    Joined:
    Mar 23, 2017
    Posts:
    72
    The case number is fine, we'll be able to see it in our system :) And thank you so much for taking the time to create a project and report this :)