Search Unity

Feedback Problems using RaycastCommand together with Jobs and some suggestions to it

Discussion in 'Scripting' started by Darkgaze, Dec 7, 2020.

  1. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    Hi all.

    I'm working on WaveMaker (my asset for simulating waves) and It needs heavy use of the Unity Job System and Raycasts.

    I encountered some problems.

    The first one is that I cannot throw rays to a specific object via RaycastCommand the same way we can do it with a single ray with Collider.Raycast. We can only pass a LayerMask. The problem with layers is that there are not many and I don't want people to add a layer just for this asset. In some projects they are extremely optimized and there are not many available. Having the possibility to pass just one object to test would be a great improvement. In fact due to the need of giving a "max number of hits" that has to be as small as possible, I get hits to unwanted objects, unless I use masks which I don't really need.
    Adding them on runtime just for this process seems a little hacky for me.

    The other problem, which makes my process slow even though I take advantage of Jobs everyhwere, is that RaycastCommand returns:

    Code (CSharp):
    1. _raycastResults = new NativeArray<RaycastHit>(size, allocationType).
    2. handle = RaycastCommand.ScheduleBatch(_raycasts, _raycastResults, 64, handle);
    But I cannot use that NativeArray with RaycastHits inside a Job to gather and manage the results because the way to tell if a reycast hit a given object, you need to access the Collider methods, which is a class that is inside UnityEngine and Jobs cannot work with those.

    In fact, this problem is tied to the previous one. Since I cannot throw rays to just one object, I'm forced to check which is the collider detected inside my job. I need an intermediate non-multithreaded process that copies all the hitData into another data struct and slowing down the process very much, because I have tens of thousands of results.

    So even though RaycastCommand was created to use with jobs, I can't work with its results at all.
    Am I missing something here?


    Thanks
     
  2. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
  3. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
    Hi darkgaze,

    I have collided with the same problem - RaycastCommand stuff works well, but iteration over whole RaycastHit array and checking raycastHit.collider != null eats whole performance :(

    The only solution that I come up with - the unsafe one. But it makes my code as 2 times faster as before (~ 1.36ms before, ~0.50ms after. What was I did - I move my check from the main thread to IJobParallerFor and checking not raycastHit.collider, but raycastHit.point. This solution is bad because you can have raycast point in Vector3.zero in theory.

    Maybe you were come up with any solution since you asked this question?

    The code of my solution

    Code (CSharp):
    1. //somewhere in Awake, because I pre calculate vaues in Commands array and length always the same for Commands and results
    2.  
    3. Results = new NativeArray<RaycastHit>(PrecalculatedWindRays.Length, Allocator.Persistent);
    4. Commands = new NativeArray<RaycastCommand>(PrecalculatedWindRays.Length, Allocator.Persistent);
    5.  
    6. //then
    7.  
    8. private void FixedUpdate ()
    9. {
    10.    RaycastCommand.ScheduleBatch(Commands, Results, 1).Complete();
    11.    NativeList<RaycastHit> validRaycastHits = new NativeList<RaycastHit>(Results.Length, Allocator.TempJob);
    12.    NativeList<RaycastHit>.ParallelWriter validRaycastHitsWriter = validRaycastHits.AsParallelWriter();
    13.  
    14.    new HitProceedJob
    15.    {
    16.       RaycastHitDataColection = Results,
    17.       ValidRaycastHits = validRaycastHitsWriter
    18.    }.Schedule(Results.Length, 32).Complete();
    19.  
    20.    for (int hitObjectPointer = 0; hitObjectPointer < validRaycastHits.Length; hitObjectPointer++)
    21.    {
    22.       RaycastHit batchedHit = validRaycastHits[hitObjectPointer];
    23.       batchedHit.rigidbody.AddForceAtPosition(Vector3.forward * 0.001f, batchedHit.point, ForceMode.Impulse);
    24.    }
    25.  
    26.    validRaycastHits.Dispose();
    27. }
    28.  
    29. [BurstCompile]
    30. private struct HitProceedJob : IJobParallelFor
    31. {
    32.    [ReadOnly]
    33.    public NativeArray<RaycastHit> RaycastHitDataColection;
    34.    [WriteOnly]
    35.    public NativeList<RaycastHit>.ParallelWriter ValidRaycastHits;
    36.  
    37.    public void Execute (int index)
    38.    {
    39.       RaycastHit currentRaycast = RaycastHitDataColection[index];
    40.      
    41.       if (currentRaycast.point != default) //it's a HACK!!! Original and right solution is currentRaycast.Collider != null, but this doesn't work in a job.
    42.       {
    43.          ValidRaycastHits.AddNoResize(currentRaycast);
    44.       }
    45.    }
    46. }
    47.  
    48. //and dispose arrays
    49.  
    50. private void OnDestroy ()
    51. {
    52.    Results.Dispose();
    53.    Commands.Dispose();
    54. }
    55.  
     
    Last edited: Jul 25, 2021
  4. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    Hi there.
    Your solution is good as long as point is ... valid. I don't know if comparing to "default" will suffice, in my case it doesn't.

    By the way, using paralell anything is much slower (depends on the percentage of calls that use it and the number of jobs writing at the same time. It is always better to have a huge array and find a way to store everything there without having to use a paralell writer.

    Here's the solution I found. It's a little hacky but works perfectly.
    http://blog.lidia-martinez.com/use-the-results-of-raycastcommand-schedule-on-a-job
     
  5. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
    Thank you for your feedback. Your way really looks like black magic but seems it works as expected + eliminates invalid coordinates comparison.
     
  6. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    The only problem with it is that you have to be aware of the changes in subsequent versions of Unity. But probably won't change. Using #if UNITY_XXXX_X_OR_NEWER or such, you will be able to have different versions of the code in case the structure of the data in RaycastHit changes...

    By the way, let me give some credit to the people at support, they gave me this solution. :)
     
  7. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46

    I'm not sure if I got your point right, but yesterday I experimented with your code. That structure - RaycastHitPublic, contains fields, that I don't really need (for example Vector3 point). And when I removed all fields except int collider from it - it worked as expected and produce the same results as original code.

    I don't really understand how this works, probably compiler is smart enough to got this case. I will send the modified code later today.
     
  8. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
    So, the original code

    Code (CSharp):
    1.         [StructLayout(LayoutKind.Sequential)]
    2.         private struct RaycastHitPublic
    3.         {
    4.             public Vector3 m_Point;
    5.             public Vector3 m_Normal;
    6.             public int m_FaceID;
    7.             public float m_Distance;
    8.             public Vector2 m_UV;
    9.             public int m_ColliderID;
    10.         }
    11.  
    12.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    13.         private static unsafe int GetColliderID (RaycastHit hit)
    14.         {
    15.             RaycastHitPublic h = *(RaycastHitPublic*) &hit;
    16.             return h.m_ColliderID;
    17.         }
    18.  
    and short one

    Code (CSharp):
    1.         [StructLayout(LayoutKind.Sequential)]
    2.         private struct RaycastHitPublic
    3.         {
    4.             public int m_ColliderID;
    5.         }
    6.  
    7.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    8.         private static unsafe int GetColliderID (RaycastHit hit)
    9.         {
    10.             RaycastHitPublic h = *(RaycastHitPublic*) &hit;
    11.             return h.m_ColliderID;
    12.         }
    13.  
    Due to my observation that short code works as expected (maybe a can do some unit test to be sure on 100%)
     
  9. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
    Btw, I found that UnsafeUtility has something like that

    Code (CSharp):
    1.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    2.         private static int GetColliderID (RaycastHit hit)
    3.         {
    4.             return UnsafeUtility.As<RaycastHit, RaycastHitPublic>(ref hit).m_ColliderID;
    5.         }
    6.  
    Seems works exactly as your code
     
  10. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    That looks awesome.... but how does it work? ... The point is that you are faking the size of each layout item so that you reach that specific one. How come this works?
     
  11. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395

    Did you check if the collider ID returned is the same?
    Because you need those fields.
    This is what you are really telling the compiler:

    private struct RaycastHitPublic
    {
    public Vector3 m_Point; <-- 3 floats = 12 bytes
    public Vector3 m_Normal; <-- 3 float = 12 bytes
    public int m_FaceID; <-- 4 bytes
    public float m_Distance; <-- 4 bytes
    public Vector2 m_UV; <-- 8 bytes
    public int m_ColliderID; <-- 4 bytes
    }


    You are telling the copiler to access an item of size "4 bytes" (m_ColliderID), after 12 + 12 + 4 + 4 + 8 = 40 bytes and interpret it as an integer to get the value. That is why we need the data until that position only, the rest doesn't matter. And that's why you say it is "Layout Sequential", so that the compiler doesn't reorder the items.

    WIth this:

    private struct RaycastHitPublic
    {
    public int m_ColliderID;
    }

    You are telling the compiler to NOT skip any data, and take the first 4 bytes starting from the pointer to memory you gave it.
    So you will get public Vector3 m_Point; first value (m_Point.x) as the Collider, which could be easily a good looking number. But it's not the collider Id.



    RaycastHitPublic h = *(RaycastHitPublic*) &hit; <.-- This line means:

    &hit <-- get the pointer (address) to memory where this data structure is stored, not the struct itself (RaycastHit)

    (RaycastHitPublic*) &hit <-- Interpret the data starting from that address in memory as a pointer to a RaycastHitPublic struct. Pointers can point anywhere so it's fine.

    *(RaycastHitPublic*) &hit <-- Now get the data pointing to that address (the contary operation to &). It will take 44 bytes of that data (the size of the new struct) which are less values than the original, but the first fit perfectly because we used the same layout as RaycastHit

    Then assign it to our new RaycastHitPublic struct.

    So you are basically telling the compiler to interpret the first part of the original RaycastHit as a RaycastHitPublic (our own struct), so that when you access m_ColliderID or whatever name you give it, it will take the Integer starting from 40 bytes.
     
    Last edited: Jul 30, 2021
  12. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
    Thanx for info. I will write some unit tests a bit later and share results. I will be really happy if there is a way to see the IL code generated by Unity, like how I can do that for common .net/core code :/
     
  13. AnKOu

    AnKOu

    Joined:
    Aug 30, 2013
    Posts:
    123
    Hi ! You say in your blog that
    But I didn't find information about it. Is that kind of under the hood stuff or is there something to code specifically ?



    It may would work better with a test like this I think :

    if (math.any( currentRaycast.normal != new float3(0,0,0) )
     
  14. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    I guess it means the change in 2021.1 where you can access RaycastHit.colliderInstanceID
     
    Darkgaze, MagicianArtemka and AnKOu like this.
  15. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    The team at Unity told me they did something. But honestly, I have no idea how to do it now... I just left that code like that until I remember to check what happened afterwards...

    [EDIT] Somebody just answered from the team. There's the answer. Thanks!
     
    MelvMay and AnKOu like this.
  16. MagicianArtemka

    MagicianArtemka

    Joined:
    Jan 15, 2019
    Posts:
    46
  17. rohan_zatun

    rohan_zatun

    Joined:
    Aug 18, 2021
    Posts:
    6
    Any chance this gets backported to 2019.4?
     
  18. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Features never get backported, only bug fixes.
     
  19. Webertob

    Webertob

    Joined:
    Dec 12, 2020
    Posts:
    9
    With respect to ColliderID I am able to get both versions up and running. However I struggle to retrieve UV coordinates through basically the same reason. It is provided as Vector2 which is not supported

    Code (CSharp):
    1.  
    2. public void Execute(ParticleSystemJobData particles)
    3.         {
    4.             var n = 0;
    5.             var drop = new Droplet();
    6.             var life = particles.aliveTimePercent;
    7.             var color = particles.startColors;
    8.             for (int i = startIndex; i < endIndex; i++) {
    9.                 var hit = rcHit[i];
    10.                 if (hit.colliderInstanceID == colliderID)
    11.                 {
    12.                     life[i] = 100.0f;
    13.                     drop.texCoord = hit.textureCoord;
    14.                     drop.color = color[i];
    15.                     droplets[n++] = drop;
    16.                 }
    17.             }
    18.             dropletsCount.Value = n;
    19.         }
    20.  
    Error message I get is:
    I am able to suppress warning by using. However the results just contains zeros!

    Code (CSharp):
    1.  
    2.  
    3. [StructLayout(LayoutKind.Sequential)]
    4.         internal struct RaycastHitPublic
    5.         {
    6.             public Vector3 m_Point;
    7.             public Vector3 m_Normal;
    8.             public int m_FaceID;
    9.             public float m_Distance;
    10.             public Vector2 m_UV;
    11.             public int m_ColliderID;
    12.         }
    13.  
    14.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    15.         public static int GetColliderID(RaycastHit hit)
    16.         {
    17.             unsafe
    18.             {
    19.                 RaycastHitPublic h = *(RaycastHitPublic*)&hit;
    20.                 return h.m_ColliderID;
    21.             }
    22.         }
    23.  
    24.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    25.         public static Vector2 GetTextureCoord(RaycastHit hit)
    26.         {
    27.             unsafe
    28.             {
    29.                 RaycastHitPublic h = *(RaycastHitPublic*)&hit;
    30.                 return h.m_UV;
    31.             }
    32.         }
    33.  
     
  20. Webertob

    Webertob

    Joined:
    Dec 12, 2020
    Posts:
    9
    After a lot of debugging directly watching UV / texture coordinates stored inside the RaycastHit I need to report the values stored in the private m_UV variable are just wrong.

    hit.textureCoord != hit.m_UV

    See debug screen shot attached.
     

    Attached Files:

  21. Webertob

    Webertob

    Joined:
    Dec 12, 2020
    Posts:
    9
    Just a note m_UV contains the last 2 elements of the barycentricCoordinates. But it should contain the first 2 elements. Seems to be a bug. I don't know if they are going to fix this as it is an non-public part of this struct. But on the otherhand what is the meaning of having it at all, when it contains wrong data.

    With respect of DOTS/Burst/Jobs access to m_UV as a value type would be more than welcome!
     
  22. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    Seems wrong, yes. But didn't have time to check what you're saying. Did you report a bug? :-(
     
  23. yant

    yant

    Unity Technologies

    Joined:
    Jul 24, 2013
    Posts:
    596
    Hey.

    m_UV is precisely the barycentric coordinates as returned by the physics engine (see https://github.com/NVIDIAGameWorks/...38fe6fd5fa/physx/include/PxQueryReport.h#L180).

    However, what you get as RaycastHit.barycentricCoordinates is just a triplet that is defined here: https://github.com/Unity-Technologi...ysics/ScriptBindings/Dynamics.bindings.cs#L51

    That said, I understand the sentiment about being able to access the data of RaycastHit from threads, that should be doable and I'm adding a card to track this work for a future release.

    Anthony.
     
    Bunny83 likes this.
  24. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    With 2021 changes its finally possible to get collider instance id. But at the same time there's no way to get actually Collider for that id, since Object.FindObjectFromInstanceID is internal.

    Wonder if reinterpreting custom structure of RaycastHit layout to RaycastHit, and then fetching collider property will do the trick. Maybe there's a simpler way?
     
  25. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    You cannot access managed classes in a job. It's not thread-safe.
     
  26. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Yeah I know. I've got a case where results of raycasting comes from two separate sources - customly written trigger interactions, and Physx based queries. They're gathered into single buffer and processed by systems. Some are managed and they run on main thread, here's where getting actual collider is required.

    I'd like to avoid storing whole RaycastHit struct, since its values aren't used, and pretty much a waste of memory for this case (and data duplication, such as extra HitPoint, HitNormal, and HitDistance stored). Plus, its user error prone.

    But without RaycastHit, there's no way to fetch collider by instance id.
     
  27. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Then you already had your answer. :) There's no avoiding it.

    I mean, there's a special access for things like Transforms (TransformAccess). I guess conceivably it could be done for specific types like Collider/Collider2D but a collider is so much more than a simple Transform so it'd be very limited on what access you'd get. Thinking out loud!
     
    LudiKha likes this.
  28. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    I need it in a managed context. In a system that runs on a main thread. No jobs done there.
     
    MelvMay likes this.
  29. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Ah right, given what you wrote I read it as also accessing the colliders off the main thread. Sorry, my misinterpretation!

    Hmm, yeah, it'd be nice to expose that. It'd likely have to be UnityEngine.Object from which you can check the type and cast it or some such.
     
    xVergilx likes this.
  30. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    That said, there's nothing stopping the 2D/3D physics teams from exposing that somehow via the RaycastHit(2D) or Collider(2D) or Rigidbody(2D) types. I'll ask "around".
     
    xVergilx likes this.
  31. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Meanwhile, I've come up with this:
    Code (CSharp):
    1. using System.Runtime.InteropServices;
    2. using UnityEngine;
    3.  
    4. namespace HitsGathering {
    5.    /// <summary>
    6.    /// Hacked version of RaycastHit, allows to obtain collider by reinterpreting
    7.    ///
    8.    /// RaycastHitHack structure to RaycastHit and obtaining collider via property
    9.    /// (since Object.FindObjectFromInstanceID is internal)
    10.    ///
    11.    /// TODO remove once exposed
    12.    /// </summary>
    13.    [StructLayout(LayoutKind.Sequential)]
    14.    public struct RaycastHitHack {
    15.       public Vector3 m_Point;
    16.       public Vector3 m_Normal;
    17.       public uint m_FaceID;
    18.       public float m_Distance;
    19.       public Vector2 m_UV;
    20.       public int m_Collider;
    21.    }
    22.  
    23.    public static class RaycastHitHackExt {
    24.      public static Collider GetColliderByInstanceId(this int instanceId) {
    25.          RaycastHitHack hack = new RaycastHitHack();
    26.          hack.m_Collider = instanceId;
    27.  
    28.          unsafe {
    29.             RaycastHit hit = *(RaycastHit*) &hack;
    30.             Collider result = hit.collider;
    31. #if DEBUG
    32.             if (result != null)
    33.                Debug.Assert(instanceId == result.GetInstanceID());
    34. #endif
    35.             return result;
    36.          }
    37.       }
    38.    }
    39. }
    40.  
    Assert matches, and it seems to return correct colliders.
    If anyone else stumbles upon it, works with 2021 - struct layout may vary based on the version used.
     
    Last edited: Oct 28, 2022
    MelvMay likes this.
  32. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Yes, I was going to suggest something like that but I didn't have the guts. :) Obviously it's super prone to breaking.
     
    xVergilx likes this.
  33. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    While we're being crazy, the reflection route:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Reflection;
    3.  
    4. public class TestReflect : MonoBehaviour
    5. {
    6.     object[] instanceIDArg = new object[1];
    7.  
    8.     void Start()
    9.     {
    10.         instanceIDArg[0] = GetInstanceID();
    11.  
    12.         var DoesObjectWithInstanceIDExist = typeof(Object).GetMethod("DoesObjectWithInstanceIDExist", BindingFlags.Static | BindingFlags.NonPublic);
    13.         if (DoesObjectWithInstanceIDExist != null)
    14.         {
    15.             var wasFound = (bool)DoesObjectWithInstanceIDExist.Invoke(null, instanceIDArg);
    16.             if (wasFound)
    17.             {
    18.                 Debug.Log("We found the object from its instance ID.");
    19.             }
    20.         }      
    21.  
    22.         var FindObjectFromInstanceID = typeof(Object).GetMethod("FindObjectFromInstanceID", BindingFlags.Static | BindingFlags.NonPublic);
    23.         if (FindObjectFromInstanceID != null)
    24.         {
    25.             var foundObj = FindObjectFromInstanceID.Invoke(null, instanceIDArg);
    26.  
    27.             if (GetType() == foundObj.GetType())
    28.             {
    29.                 Debug.Log("We found the object from its instance ID.");
    30.             }
    31.         }
    32.     }
    33. }
    34.  
     
    xVergilx likes this.
  34. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    For some reason GetHashCode() in Android builds does not actually resolve to instance id which trips the Assert.
    Probably an intended behaviour (GetHashCode() gets patched or smth?) but in any case edited this to GetInstanceId() for safety reasons. Speed in ns does not matter in editor / debug environment anyway.
     
  35. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    395
    There is no documentation page for 2021.1 . I always look for stuff this way... Let me tell you why .... Because searching for that easy change on the unity ChangeLog page is IMPOSSIBLE. (Please, please, make it easier to find changes related to X... better filters... etc)

    So I guess it is only available on 2021.2+ ?
     
  36. Djik

    Djik

    Joined:
    Jul 13, 2010
    Posts:
    19
  37. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Being as you specifically tagged me in I have to tell you that I am not part of the 3D physics team nor would I have any way of implementing what you just asked for.