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

Resolved Abysmal performance with raycasting in the new Unity Physics (ECS)

Discussion in 'Physics for ECS' started by Interjection, Jul 3, 2020.

  1. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    Since RaycastCommand is broken (can't receive multiple hits from a ray while the docs says it can) I thought that maybe I can use the new "Unity Physics" to raycast faster but in my test the old PhysX "Physics.RaycastNonAlloc()" wins over Unity Physic's "CollisionWorld.CastRay()" (which is a linecast).

    What takes PhysX 19 ms takes Unity Physics 885 ms on my machine. That's over 46 times longer.
    However, none of my test code is jobified or burst compiled and I've only tested in the Editor. I had hoped it was possible to only use Unity Physics in my game for faster raycasting purposes and keep the rest of my game "non-dots" (because making it "all dots" now would take a lot of effort).

    Am I doing something wrong? What is the preferred method of doing raycasts in Unity Physics?
    Or can't it be helped that communicating with the "dots" system from "non-dots" code is this slow?
     
  2. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    Here's my test code. doFireNewSystem() is where I fire the ray in the new Unity Physics.

    You can test this by adding my RayBomber script to any GameObject in the scene. You just need a plane at the top named "RayPlane" (without a collider) and three planes below it (with colliders and Unity's Convert-To-Entity script).

    Edit: I forgot to mention that ConvertToEntity must have Conversion Mode set to "Convert and Inject GameObject" in order for my test to work properly.

    Edit 2: OLD CODE! Look below for a newer version of RayBomber.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RayBomber : MonoBehaviour {
    4.  
    5.     private Transform rayPlaneTransform;
    6.     private Unity.Collections.NativeList<Unity.Physics.RaycastHit> nativeArray;
    7.     private Unity.Physics.Systems.BuildPhysicsWorld bpw;
    8.  
    9.     private System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    10.     private System.Collections.Generic.List<double> millisecondsOld = null;
    11.     private System.Collections.Generic.List<double> millisecondsNew = null;
    12.     private Unity.Physics.RaycastInput raycastInput;
    13.  
    14.     private UnityEngine.RaycastHit[] hits = new UnityEngine.RaycastHit[10];
    15.     private bool allowMessagePrinting = true;
    16.     private bool useOldSystem = true;
    17.     private int cooldown = 4;
    18.  
    19.     void Start() {
    20.         rayPlaneTransform = GameObject.Find("RayPlane").transform;  //raycast downwards to y=0 from this Transform
    21.        nativeArray = new Unity.Collections.NativeList<Unity.Physics.RaycastHit>(0, Unity.Collections.Allocator.Persistent);
    22.         bpw = Unity.Entities.World.DefaultGameObjectInjectionWorld.GetExistingSystem<Unity.Physics.Systems.BuildPhysicsWorld>();
    23.         raycastInput = new Unity.Physics.RaycastInput {
    24.             Filter = new Unity.Physics.CollisionFilter {
    25.                 BelongsTo = ~0u,
    26.                 CollidesWith = ~0u,
    27.                 GroupIndex = 0
    28.             }
    29.         };
    30.     }
    31.  
    32.     void Update() {
    33.         sw.Reset();
    34.         for (int i = 0; i < 9999; i++) {
    35.             fireRandomRay();
    36.         }
    37.         if (millisecondsOld!=null) {
    38.             if (useOldSystem) {
    39.                 millisecondsOld.Add(sw.ElapsedTicks/10000.0);
    40.             } else {
    41.                 millisecondsNew.Add(sw.ElapsedTicks/10000.0);
    42.             }
    43.         } else {
    44.             useOldSystem = !useOldSystem;  //warm up
    45.         }
    46.     }
    47.  
    48.     public void fireRandomRay() {
    49.         Vector3 rayPlanePos = rayPlaneTransform.position;
    50.         float xSize = 10 * rayPlaneTransform.localScale.x;
    51.         float zSize = 10 * rayPlaneTransform.localScale.z;
    52.        Vector3 from = new Vector3(UnityEngine.Random.value*xSize - xSize/2f, rayPlanePos.y, UnityEngine.Random.value*zSize - zSize/2f);
    53.         Vector3 to = new Vector3(from.x, 0, from.z);
    54.         Debug.DrawLine(from, to, useOldSystem ? Color.yellow : Color.green);
    55.         if (useOldSystem) {
    56.             doFireOldSystem(from, to);
    57.         } else {
    58.             doFireNewSystem(from, to);
    59.         }
    60.     }
    61.  
    62.     private void doFireOldSystem(Vector3 from, Vector3 to) {
    63.         sw.Start();
    64.         int numHits = Physics.RaycastNonAlloc(from, Vector3.down, hits, from.y-to.y);
    65.         if (numHits!=3) {
    66.             printWarning(false);
    67.         }
    68.         sw.Stop();
    69.     }
    70.  
    71.     private void doFireNewSystem(Vector3 from, Vector3 to) {
    72.         sw.Start();
    73.         raycastInput.Start = from;
    74.         raycastInput.End = to;
    75.         nativeArray.Clear();
    76.         if (bpw.PhysicsWorld.CollisionWorld.CastRay(raycastInput, ref nativeArray)) {
    77.             if (nativeArray.Length!=3) {
    78.                 printWarning(true);
    79.             }
    80.         } else {
    81.             printWarning(false);
    82.         }
    83.         sw.Stop();
    84.     }
    85.  
    86.     void OnDestroy() {
    87.         nativeArray.Dispose();
    88.     }
    89.  
    90.     void LateUpdate() {
    91.         if (millisecondsOld==null) {
    92.             cooldown--;
    93.             if (cooldown==0) {
    94.                 millisecondsOld = new System.Collections.Generic.List<double>();
    95.                 millisecondsNew = new System.Collections.Generic.List<double>();
    96.             }
    97.         } else {
    98.             System.Collections.Generic.List<double> daList = useOldSystem ? millisecondsOld : millisecondsNew;
    99.             double sum = 0;
    100.             for (int i = 0; i < daList.Count; i++) {
    101.                 sum += daList[i];
    102.             }
    103.             Debug.Log((sum/daList.Count) +" ms "+ (useOldSystem ? "(old)" : "(new)"));
    104.             useOldSystem = !useOldSystem;
    105.         }
    106.     }
    107.  
    108.     private void printWarning(bool notThreeHits) {
    109.         if (allowMessagePrinting) {
    110.             allowMessagePrinting = false;
    111.             Debug.LogWarning((notThreeHits ? "Not three hits!" : "No hit!") + (useOldSystem ? " (old)" : " (new)"));
    112.         }
    113.     }
    114.  
    115. }
    116.  
    editor.png

    (For visitors from the future, I ran the code above in "Unity 2019.4.0f1 (64-bit)", which was released June 2020. Wouldn't surprise me if it no longer compiles in a year or two since they are still changing things around.)
     
    Last edited: Jul 4, 2020
  3. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    I have also tried to test the performance of raycasting inside a job with the burst compiler btw, but if I try to store a reference to
    Unity.Physics.Systems.BuildPhysicsWorld
    in a field of the IJob struct I get the error "InvalidOperationException: UnityPhysicsRaycastingJob.bpw is not a value type. Job structs may not contain any reference types." and if I try to get the world inside the job's Execute() via
    Unity.Physics.CollisionWorld cw = Unity.Entities.World.DefaultGameObjectInjectionWorld.GetExistingSystem<Unity.Physics.Systems.BuildPhysicsWorld>().PhysicsWorld.CollisionWorld;
    I get the error "Burst error BC1016: The managed function `Unity.Entities.World.get_DefaultGameObjectInjectionWorld()` is not supported".

    How do you raycast with Unity Physics inside a job if you can't access the world?
     
  4. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    Try storing a CollisionWorld and RaycastInput in the job. They are value types and therefore Burst-friendly. BuildPhysicsWorld can't be provided to Burst compiled jobs.

    Also, when you are measuring performance of Burst jobs, make sure leak detection, safety checks and jobs debugger are all off. Also, editor will give you slightly slower performance compared to standalone.
     
  5. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    Of course! Why didn't I think of that? I've rewritten RayBomber to include a test of UnityPhysics in a burst-compiled multi-threaded job and it wins over PhysX both in the editor (where UnityPhysics in a job took ~16 ms) and in an exe (where it took ~8 ms). PhysX took 25 ms in the editor and 20 ms in an exe.

    Here's the newer version of RayBomber.

    Note that the time measurement of raycasting in jobified Unity Physics includes preparing the rays and making the results ready for the main thread. This is intentional since it gives a fair comparison that way, rather than only measuring the actual raycasting. It's how it would be used in a real game.

    Edit: Same scene as described in my second post. Code compiled using "Unity 2019.4.0f1 (64-bit)".
    Edit 2: Minor update to the code.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RayBomber : MonoBehaviour {
    4.  
    5.    private static readonly int RAYS_PER_FRAME = 9999;
    6.    private static readonly int MAX_HITS_PER_RAY = 50;
    7.    private Unity.Collections.NativeList<Unity.Physics.RaycastInput> _nativeRaycastInput_List;
    8.    private Unity.Collections.NativeList<Unity.Physics.RaycastHit> _nativeRaycastHit_List;
    9.    private Unity.Physics.Systems.BuildPhysicsWorld _bpw;
    10.    private UnityPhysicsRaycastingJob _theJob;
    11.  
    12.    private System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    13.    private System.Collections.Generic.List<double> millisecondsPhysX = new System.Collections.Generic.List<double>();
    14.    private System.Collections.Generic.List<double> millisecondsUnityPhysics = new System.Collections.Generic.List<double>();
    15.    private System.Collections.Generic.List<double> millisecondsUnityPhysicsInJob = new System.Collections.Generic.List<double>();
    16.    private Unity.Physics.RaycastInput raycastInput;
    17.  
    18.    private UnityEngine.RaycastHit[] physXhits = new UnityEngine.RaycastHit[MAX_HITS_PER_RAY];
    19.    private bool[] disallowWarningToBePrinted;
    20.    private enum SYSTEM { PhysX, UnityPhysics, UnityPhysicsInJob }
    21.    private SYSTEM testThisSystem = SYSTEM.PhysX;
    22.    private int cooldown = 4;
    23.  
    24.    private float rayY;
    25.    private float rayXSize;
    26.    private float rayZSize;
    27.    private int numberOfSystemsTested;
    28.    private string[] presentation;
    29.  
    30.    void Start() {
    31.       Transform rayPlaneTransform = GameObject.Find("RayPlane").transform;  //raycast downwards to y=0 from this Transform
    32.       _nativeRaycastInput_List = new Unity.Collections.NativeList<Unity.Physics.RaycastInput>(10, Unity.Collections.Allocator.Persistent);
    33.       _nativeRaycastHit_List = new Unity.Collections.NativeList<Unity.Physics.RaycastHit>(10, Unity.Collections.Allocator.Persistent);
    34.       _bpw = Unity.Entities.World.DefaultGameObjectInjectionWorld.GetExistingSystem<Unity.Physics.Systems.BuildPhysicsWorld>();
    35.       _theJob = new UnityPhysicsRaycastingJob() {
    36.          input = _nativeRaycastInput_List,
    37.          rayHits = new Unity.Collections.NativeArray<int>(RAYS_PER_FRAME, Unity.Collections.Allocator.Persistent),
    38.          output = new Unity.Collections.NativeArray<Unity.Physics.RaycastHit>(RAYS_PER_FRAME * MAX_HITS_PER_RAY, Unity.Collections.Allocator.Persistent)
    39.       };
    40.       raycastInput = new Unity.Physics.RaycastInput {
    41.          Filter = new Unity.Physics.CollisionFilter {
    42.             BelongsTo = ~0u,
    43.             CollidesWith = ~0u,
    44.             GroupIndex = 0
    45.          }
    46.       };
    47.       rayY = rayPlaneTransform.position.y;
    48.       rayXSize = 10 * rayPlaneTransform.localScale.x;
    49.       rayZSize = 10 * rayPlaneTransform.localScale.z;
    50.       numberOfSystemsTested = System.Enum.GetNames(typeof(SYSTEM)).Length;
    51.       presentation = new string[numberOfSystemsTested];
    52.       disallowWarningToBePrinted = new bool[numberOfSystemsTested];
    53.    }
    54.  
    55.    void Update() {
    56.       sw.Reset();
    57.       if (testThisSystem==SYSTEM.PhysX || testThisSystem==SYSTEM.UnityPhysics) {
    58.          for (int i = 0; i < RAYS_PER_FRAME; i++) {
    59.             fireRandomRay();
    60.          }
    61.       } else {  //SYSTEM.UnityPhysicsInJob
    62.          fireRandomRaysInJob();
    63.       }
    64.       collectResults();
    65.    }
    66.  
    67.    private void collectResults() {
    68.       if (cooldown==0) {
    69.          //collect results
    70.          if (testThisSystem==SYSTEM.PhysX) {
    71.             millisecondsPhysX.Add(sw.ElapsedTicks/10000.0);
    72.          } else if (testThisSystem==SYSTEM.UnityPhysics) {
    73.             millisecondsUnityPhysics.Add(sw.ElapsedTicks/10000.0);
    74.          } else {  //SYSTEM.UnityPhysicsInJob
    75.             millisecondsUnityPhysicsInJob.Add(sw.ElapsedTicks/10000.0);
    76.          }
    77.       } else {
    78.          //warm up the first few LateUpdates
    79.          testNextSystem();
    80.       }
    81.    }
    82.  
    83.    private void testNextSystem() {
    84.       testThisSystem++;
    85.       if ((int)testThisSystem==numberOfSystemsTested) {
    86.          testThisSystem = 0;
    87.       }
    88.    }
    89.  
    90.    private void fireRandomRay() {
    91.       Vector3 from;
    92.       Vector3 to;
    93.       sw.Start();
    94.       {
    95.          from = randomizeRayOrigin();
    96.          to = new Vector3(from.x, 0, from.z);
    97.          if (testThisSystem==SYSTEM.PhysX) {
    98.             doFirePhysXSystem(from, to);
    99.          } else {  //SYSTEM.UnityPhysics
    100.             doFireUnityPhysicsSystem(from, to);
    101.          }
    102.       }
    103.       sw.Stop();
    104.       if (testThisSystem==SYSTEM.PhysX) {
    105.          Debug.DrawLine(from, to, Color.yellow);
    106.       } else {  //SYSTEM.UnityPhysics
    107.          Debug.DrawLine(from, to, Color.green);
    108.       }
    109.    }
    110.  
    111.    private Vector3 randomizeRayOrigin() {
    112.       return new Vector3(UnityEngine.Random.value*rayXSize - rayXSize/2f, rayY, UnityEngine.Random.value*rayZSize - rayZSize/2f);
    113.    }
    114.  
    115.    private void doFirePhysXSystem(Vector3 from, Vector3 to) {
    116.       int numHits = Physics.RaycastNonAlloc(from, Vector3.down, physXhits, from.y-to.y);
    117.       if (numHits!=3) {
    118.          printWarning(numHits);
    119.       }
    120.    }
    121.  
    122.    private void doFireUnityPhysicsSystem(Vector3 from, Vector3 to) {
    123.       raycastInput.Start = from;
    124.       raycastInput.End = to;
    125.       _nativeRaycastHit_List.Clear();
    126.       if (_bpw.PhysicsWorld.CollisionWorld.CastRay(raycastInput, ref _nativeRaycastHit_List)) {
    127.          if (_nativeRaycastHit_List.Length!=3) {
    128.             printWarning(_nativeRaycastHit_List.Length);
    129.          }
    130.       } else {
    131.          printWarning(0);
    132.       }
    133.    }
    134.  
    135.    void OnDestroy() {
    136.       _nativeRaycastInput_List.Dispose();
    137.       _nativeRaycastHit_List.Dispose();
    138.       _theJob.rayHits.Dispose();
    139.       _theJob.output.Dispose();
    140.    }
    141.  
    142.    void LateUpdate() {
    143.       if (cooldown==0) {
    144.          System.Collections.Generic.List<double> daList;
    145.          if (testThisSystem==SYSTEM.PhysX) {
    146.             daList = millisecondsPhysX;
    147.          } else if (testThisSystem==SYSTEM.UnityPhysics) {
    148.             daList = millisecondsUnityPhysics;
    149.          } else {  //SYSTEM.UnityPhysicsInJob
    150.             daList = millisecondsUnityPhysicsInJob;
    151.          }
    152.          double sum = 0;
    153.          for (int i = 0; i < daList.Count; i++) {
    154.             sum += daList[i];
    155.          }
    156.          presentation[(int)testThisSystem] = testThisSystem +": "+ (sum/daList.Count) +" ms";
    157.          Debug.Log(presentation[(int)testThisSystem]);
    158.          testNextSystem();
    159.       } else {
    160.          //warm up the first few LateUpdates
    161.          cooldown--;
    162.          testNextSystem();
    163.       }
    164.    }
    165.  
    166.    void OnGUI() {
    167.       GUIStyle style = new GUIStyle();
    168.       style.fontSize = 20;
    169.       style.fontStyle = FontStyle.Bold;
    170.       for (int i = 0; i < 2; i++) {
    171.          style.normal.textColor = i==0 ? Color.black : Color.white;
    172.          if (cooldown==0) {
    173.             for (int j = 0; j < numberOfSystemsTested; j++) {
    174.                GUI.Label(new Rect(10-i, (10-i) + 20*j, Screen.width-10, 20), presentation[j], style);
    175.             }
    176.          } else {
    177.             GUI.Label(new Rect(10-i, 10-i, Screen.width-10, 20), "Warming up... "+ cooldown, style);
    178.          }
    179.       }
    180.    }
    181.  
    182.    private void printWarning(int numHits) {
    183.       if (!disallowWarningToBePrinted[(int)testThisSystem]) {
    184.          disallowWarningToBePrinted[(int)testThisSystem] = true;
    185.          Debug.LogWarning("Not three hits! Got "+ numHits +". Faulty test. ("+ testThisSystem +")");
    186.       }
    187.    }
    188.  
    189.    private void fireRandomRaysInJob() {
    190.       int RAYS_PER_THREAD = RAYS_PER_FRAME / 100 + 1;
    191.       sw.Start();
    192.       {
    193.          _nativeRaycastInput_List.Clear();
    194.          for (int i = 0; i < RAYS_PER_FRAME; i++) {
    195.             Vector3 from = randomizeRayOrigin();
    196.             Vector3 to = new Vector3(from.x, 0, from.z);
    197.             raycastInput.Start = from;
    198.             raycastInput.End = to;
    199.             _nativeRaycastInput_List.Add(raycastInput);
    200.          }
    201.          _theJob.cw = _bpw.PhysicsWorld.CollisionWorld;
    202.          Unity.Jobs.JobHandle handle = Unity.Jobs.IJobParallelForExtensions.Schedule<UnityPhysicsRaycastingJob>(_theJob, RAYS_PER_FRAME, RAYS_PER_THREAD);
    203.          handle.Complete();
    204.       }
    205.       sw.Stop();
    206.       for (int i = 0; i < RAYS_PER_FRAME; i++) {
    207.          if (_theJob.rayHits[i]!=3) {
    208.             printWarning(_theJob.rayHits[i]);
    209.          }
    210.          //could call Debug.DrawLine(); here to visualize the shot rays but i'll skip it
    211.       }
    212.    }
    213.  
    214.    [Unity.Burst.BurstCompile(CompileSynchronously = true)]
    215.    private struct UnityPhysicsRaycastingJob : Unity.Jobs.IJobParallelFor {
    216.       [Unity.Collections.ReadOnly]
    217.       public Unity.Physics.CollisionWorld cw;
    218.       [Unity.Collections.ReadOnly]
    219.       public Unity.Collections.NativeList<Unity.Physics.RaycastInput> input;
    220.       [Unity.Collections.WriteOnly]
    221.       public Unity.Collections.NativeArray<int> rayHits;
    222.       [Unity.Collections.WriteOnly]
    223.       [Unity.Collections.NativeDisableParallelForRestriction]  //means i'm sure there won't be a race condition when going outside the thread's index range
    224.       public Unity.Collections.NativeArray<Unity.Physics.RaycastHit> output;
    225.       public void Execute(int index) {
    226.          Unity.Collections.NativeList<Unity.Physics.RaycastHit> resultsList = new Unity.Collections.NativeList<Unity.Physics.RaycastHit>(10, Unity.Collections.Allocator.Temp);
    227.          if (cw.CastRay(input[index], ref resultsList)) {
    228.             int i = 0;
    229.             for (; i < resultsList.Length && i<MAX_HITS_PER_RAY; i++) {
    230.                output[index * MAX_HITS_PER_RAY + i] = resultsList[i];
    231.             }
    232.             rayHits[index] = i;
    233.          } else {
    234.             rayHits[index] = 0;
    235.          }
    236.          resultsList.Dispose();
    237.       }
    238.    }
    239.  
    240. }

    Here's a cropped screenshot from running as an exe:

    comparison.png
     
    Last edited: Jul 6, 2020
  6. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    The ~16 ms in the editor is with leak detection, safety checks and jobs debugger all turned on btw, because I didn't check how to actually turn those off yet. :p I assume they are off in the exe.

    As impressively fast raycasting in Unity Physics can be while inside a burst-compiled multi-threaded job it will take a lot of effort to rewrite my game in such a way that it becomes possible to do it.

    If anyone knows any tips and tricks that speeds up raycasting in the new Unity Physics without jobifying (meaning something that just replaces Physics.RaycastNonAlloc() in the main thread), please tell me! My tested 300 ms must be reduced below 20 ms in the exe for it to be worth switching to Unity Physics without jobs, seems impossible.
     
  7. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    I tried to raycast through 30 planes rather than 3 and instead of jobified Unity Physics being only 2.5 times faster than the built-in PhysX it was 6 times faster (in the exe). So Unity Physics scales much better. Looks like I'll have to bite the bullet and rewrite my game, the performance increase is just too good to pass on.

    (I put a cube in the scene that rotates 45 degrees every frame just so I could see the frame duration differences better.)

    30planes.png

    Edit: I'm marking the thread as "Resolved" rather than "Help Wanted" but if anyone have advice of how to improve performance of the new Unity Physics without using jobs please post!
     
    Last edited: Jul 6, 2020
    steveeHavok likes this.
  8. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    Just a side note, if you need only the closest hit from the ray, just use a different CastRay implementation (that returns a RaycastHit) and performance will go up even more. :)
     
    mmonly and steveeHavok like this.
  9. cj-currie

    cj-currie

    Joined:
    Nov 27, 2008
    Posts:
    337
    Necroposting to link a spot in the docs where code examples show how to jobify bursted raycasts from the main thread. Note that jobs can only be created in classes that inherit from SystemBase, which is the hybrid type for code that uses MonoBehaviours (managed) and ECS (unmanaged) at the same time.

    Note you'll need to complete the Broadphase PrepareStaticBodyDataJob as a dependency by calling
    state.EntityManager.CompleteDependencyBeforeRO<PhysicsWorldSingleton>();
    before raycasting as written above. Once per frame (at the start of OnUpdate() perhaps) is fine.
     
    Last edited: Jan 6, 2024