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

PhysX determinism and resetting Rigidbodies correctly

Discussion in 'Physics' started by Iron-Warrior, Jul 31, 2019.

  1. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    Hello,

    I am working on building a replay system for my game, which uses PhysX. I'm aware of that various challenges of replicating physics across different machines/builds, but for now I'm only concerned with being able to replay locally on the same machine, same build (this has enough value to allow users to create videos, or allow the dev team to capture trailer footage).

    There's lots of discussion online about whether PhysX is/is not deterministic, but the general conclusion seems to be that it is locally deterministic so long as the Enable Enhanced Determinism flag is checked.

    I have built a simple test environment where objects are exploded from the center of the scene (code below). When they all come to a rest, their positions and rotations are recorded, the rigidbodies are reset, and the explosion occurs again.



    After each run the delta position and rotations are compared from the last time to see if they are the same. So far the answer is...sort of.



    I get a few misses each time, but they are the exact same misses, with the same deltas. As well, if I manually debug out the position/rotation of the objects that missed, I find that they oscillate between two different orientations:

    Code (csharp):
    1.  
    2. Rock (1) end of sim status: pos: (0.5302396, 0.9877197, -4.5893560) rot (0.5090225, 0.5429553, 0.6523935, -0.1431022)
    3. Rock (1) end of sim status: pos: (0.5244906, 0.9944359, -4.5884690) rot (0.5166823, 0.5373523, 0.6508234, -0.1439472)
    4. Rock (1) end of sim status: pos: (0.5302396, 0.9877197, -4.5893560) rot (0.5090225, 0.5429553, 0.6523935, -0.1431022)
    5. Rock (1) end of sim status: pos: (0.5244906, 0.9944359, -4.5884690) rot (0.5166823, 0.5373523, 0.6508234, -0.1439472)
    (Rock (1) is an object that "missed"). The above is the orientations of the object after each sim.

    This has led me to believe I am not correctly resetting the rigidbodies, or not doing it at the right time during the frame, or something along those lines, since it appears that the simulation is deterministic, but oscillating between two different states.

    Entire code I am using is below:

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. public class ExperimentReplaySystem : MonoBehaviour
    5. {
    6.     [SerializeField]
    7.     int explosions = 10;
    8.  
    9.     [SerializeField]
    10.     float explosionForce = 5;
    11.  
    12.     [SerializeField]
    13.     float upwards = 5;
    14.  
    15.     [SerializeField]
    16.     float radius = 5;
    17.  
    18.     [SerializeField]
    19.     bool verbose;
    20.  
    21.     [SerializeField]
    22.     bool rigidbodyResetVerbose;
    23.  
    24.     [SerializeField]
    25.     bool autoReset;
    26.  
    27.     private class TrackedRigidbody
    28.     {
    29.         public Rigidbody rb;
    30.  
    31.         public Vector3 initialPosition;
    32.         public Quaternion initialRotation;
    33.  
    34.         public Vector3 previousFinalPosition;
    35.         public Quaternion previousFinalRotation;
    36.  
    37.         public void LogAndReset(bool verbose)
    38.         {
    39.             if (verbose)
    40.             {
    41.                 Debug.Log(string.Format("{0} end of sim status: pos: {1} rot {2}", rb.name, rb.position.ToString("F7"), rb.rotation.ToString("F7")));
    42.             }
    43.  
    44.             previousFinalPosition = rb.position;
    45.             previousFinalRotation = rb.rotation;
    46.  
    47.             rb.position = initialPosition;
    48.             rb.rotation = initialRotation;
    49.  
    50.             rb.velocity = Vector3.zero;
    51.             rb.angularVelocity = Vector3.zero;
    52.         }
    53.  
    54.         public float DeltaPosition()
    55.         {
    56.             return Vector3.Distance(previousFinalPosition, rb.position);
    57.         }
    58.  
    59.         public float DeltaRotation()
    60.         {
    61.             return Quaternion.Angle(previousFinalRotation, rb.rotation);
    62.         }
    63.     }
    64.  
    65.     private List<TrackedRigidbody> trackedRigidbodies;
    66.  
    67.     private int simulationCount = 0;
    68.     private bool simulating = false;
    69.  
    70.     private void Awake()
    71.     {
    72.         trackedRigidbodies = new List<TrackedRigidbody>();
    73.  
    74.         foreach (Rigidbody rb in FindObjectsOfType<Rigidbody>())
    75.         {
    76.             TrackedRigidbody trackedRigidbody = new TrackedRigidbody()
    77.             {
    78.                 rb = rb,
    79.                 initialPosition = rb.position,
    80.                 initialRotation = rb.rotation
    81.             };
    82.  
    83.             trackedRigidbodies.Add(trackedRigidbody);
    84.         }
    85.  
    86.         Explode();
    87.     }
    88.  
    89.     private void Update()
    90.     {
    91.         if (simulating)
    92.         {
    93.             bool stillRunning = false;
    94.  
    95.             foreach (TrackedRigidbody tracked in trackedRigidbodies)
    96.             {
    97.                 if (!tracked.rb.IsSleeping())
    98.                 {
    99.                     stillRunning = true;
    100.                     break;
    101.                 }
    102.             }
    103.  
    104.             if (!stillRunning)
    105.             {
    106.                 simulating = false;
    107.                 Evaluate();
    108.             }
    109.         }
    110.  
    111.         if (Input.GetKeyDown(KeyCode.R) && !simulating)
    112.         {
    113.             Reset();
    114.         }
    115.     }
    116.  
    117.     private void Explode()
    118.     {
    119.         Debug.Log(string.Format("Running simulation {0}", simulationCount));
    120.  
    121.         simulating = true;
    122.  
    123.         foreach (TrackedRigidbody tracked in trackedRigidbodies)
    124.         {
    125.             tracked.rb.AddExplosionForce(explosionForce, transform.position, radius, upwards, ForceMode.Impulse);
    126.         }
    127.     }
    128.  
    129.     private void Reset()
    130.     {
    131.         foreach (TrackedRigidbody tracked in trackedRigidbodies)
    132.         {
    133.             tracked.LogAndReset(rigidbodyResetVerbose);
    134.         }
    135.  
    136.         Explode();
    137.     }
    138.  
    139.     private void Evaluate()
    140.     {
    141.         if (simulationCount > 0)
    142.         {
    143.             int misses = 0;
    144.             float largestDeltaP = 0;
    145.             float largestDeltaR = 0;
    146.  
    147.             foreach (TrackedRigidbody tracked in trackedRigidbodies)
    148.             {
    149.                 float deltaP = tracked.DeltaPosition();
    150.                 float deltaR = tracked.DeltaRotation();
    151.  
    152.                 bool isMiss = deltaP != 0 || deltaR != 0;
    153.  
    154.                 largestDeltaP = Mathf.Max(deltaP, largestDeltaP);
    155.                 largestDeltaR = Mathf.Max(deltaR, largestDeltaR);
    156.  
    157.                 if (isMiss)
    158.                     misses++;
    159.  
    160.                 if (verbose)
    161.                 {
    162.                     Debug.Break();
    163.  
    164.                     string message = string.Format("Rb {0} DP: {1} DR: {2}", tracked.rb.name, deltaP.ToString("F7"), deltaR.ToString("F7"));
    165.  
    166.                     if (isMiss)
    167.                         Debug.LogError(message);
    168.                     else
    169.                         Debug.Log(message);
    170.                 }              
    171.             }
    172.  
    173.             if (misses > 0)
    174.             {
    175.                 Debug.LogError(string.Format("Encountered {0} misses. Largest deltaP: {1} Largest deltaR {2}", misses, largestDeltaP.ToString("F7"), largestDeltaR.ToString("F7")));
    176.             }
    177.             else
    178.             {
    179.                 Debug.Log("Encountered no misses.");
    180.             }
    181.         }
    182.         else
    183.         {
    184.             Debug.Log("Is first run of simulation. Next run will debug deltas.");
    185.         }
    186.  
    187.         simulationCount++;
    188.  
    189.         if (autoReset)
    190.             Reset();
    191.     }
    192.  
    193.     private void OnDrawGizmosSelected()
    194.     {
    195.         Gizmos.color = Color.yellow;
    196.         Gizmos.DrawWireSphere(transform.position, radius);
    197.     }
    198. }
    I am assuming that resetting position, rotation, velocity, and angular velocity is enough. Am I incorrect?

    Thanks for any insight,
    Erik
     
  2. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    Figured out the issue, partially. Resetting the rigidbody was not working exactly correctly, causing a divergence.

    Code (csharp):
    1. initialPosition = (0.76, 3.66, -1.73)
    2. rb.position = (0.7599999, 3.66, -1.73)
    Here is a snapshot of the Vector3 initialPosition, which is set to the rigidbody's position on Awake. Below is the position of the rigidbody after it has been reset, running the following code:

    Code (csharp):
    1. rb.position = initialPosition;
    Now, it's easy enough to tell that 0.76 is not going to be a valid floating point value, using something like this converter. So my question is: where did it get that number from? I get that it's the initial position in the editor:

    upload_2019-7-31_13-58-21.png

    And I'm guessing Unity stores those as strings (?), but how did it get into the Vector3? Does Transform not store the position at all, and only the matrix representation, creating the position when necessary?

    EDIT: My rigidbodies are nested inside a root object that is at 0,0,0 (for organization)...and now that I think about it, transform.position must be calculated on the fly, since it's the world position. Hmmm.
     
    Last edited: Jul 31, 2019
  3. TheonlysiQ

    TheonlysiQ

    Joined:
    Jul 17, 2016
    Posts:
    5
    I am doing something quite similar. Hoping you still are active on the forum actually. Did you find a solution for your problem?

    In my case, I have a number of ragdols which have forces applied on their rigidbodies' components. This causes them to move randomly on the map.

    I am using exactly the same forces to move them, but somehow, in different simmulations, after resetting them to their original positions and having the same forces applied, they end up in different places.

    Quite frustrating, since if i restart the whole scene (stop and play), the first simmulation causes the ragdols to end up always in the same position. So its definitely something related to resetting the position that's causing the issue.

    This is quite important to me and i have been stuck on it for some weeks. Can't seem to figure out an answer. Any idea would be helpful...
     
  4. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    I did actually end up figuring it out, and we do have deterministic replays (on the same build, same CPU architecture).

    One gotcha I missed was that the objects in a scene are iterated over in different orders depending on how the scene was loaded. When you push play in the editor, objects are iterated top to bottom; when you load the scene via a script (in editor or in build), it's bottom to top (I might have the orders reversed, but you get the idea). More on this topic here.
     
  5. TheonlysiQ

    TheonlysiQ

    Joined:
    Jul 17, 2016
    Posts:
    5
    Thanks for the reply, Eric.

    I had a look at your replay video. It looks awesome!

    For my case, I am interested in determinism on same machine level only.

    The article you shared provided some interesting info. I was not aware that the order of the initialization of the game objects in the world can impact the determinism of a simulation.

    I will attempt to achieve determinism again, with this additional info and your comment in mind. Hopefully doing everything the article says will be enough to cause the same behavior for my simulation when the same forces are applied as input.

    And about the iteration, I am not sure how to proceed with the order. The thing is my ragdol objects are being instantiated in the script, based on a prop i set in the inspector (the number of ragdols). So even if I push the play button or if i load the scene via a script, I would assume the order is the same? What's your opinion on this?

    My little project seems to be a lot more ambitious than i thought it would be. I have doubts i can actually achieve it now... Seems its a lot of trouble to get determinism for a 2d world physics system, whereas I am using a 3d world one.

    If you can remember anything else, or any other gotcha's like the one you already mentioned, please let me know here. I sure need all the help i can get, and resources and info on this topic are rather scarce on the forums.
     
  6. TheonlysiQ

    TheonlysiQ

    Joined:
    Jul 17, 2016
    Posts:
    5


    Here is a little demo of my simulation.When the next simulation starts, the dummies end up in totally different places. Oh its frustrating.. Its impossible for my AI to actually learn from the simulations if the same input yields different results. It's not going to get smarter, it can only get dumber like this..

    [Side note] I had a look on your game trailer on steam. The idea is brilliant! I'll definitely want to try it after i finish my thesis. Early access or not. Keep up the good work!
     
  7. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    I have a small doc that I've compiled about how to make Unity deterministic. Overall, I don't heavily recommend doing it—it's a lot of work hunting down desyncs, and it's probably easier to just make the state of your game serializable/deserializable.
     
    jamespaterson and TheonlysiQ like this.
  8. TheonlysiQ

    TheonlysiQ

    Joined:
    Jul 17, 2016
    Posts:
    5
    Thank you for this! Ill try to do it and if I fail I'll probably just reload the scene or find a way to serialize the data, like you suggested.
     
  9. TheonlysiQ

    TheonlysiQ

    Joined:
    Jul 17, 2016
    Posts:
    5
    Thesis is over, I ended up resetting the scene to get the same results. Also for the replay I use the same class to move the character (the same one as in the normal simulation mode). Basically reusing everything seemed to increase the chances of having consistency. More importantly, found more info about physx being basically rarely able to achieve consistency - https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/Manual/BestPractices.html (last fragment concerning determinism).

    Like I said, now i look forward to checking out your game. Downloading it from discord atm!