Search Unity

RaycastCommand will only return a maximum of 1 hit regardless what you set maxHits to

Discussion in 'Entity Component System' started by KieranDA, May 23, 2018.

  1. KieranDA

    KieranDA

    Joined:
    Feb 1, 2018
    Posts:
    16
    I'm just trying out async raycasts in 2018.2.0b4 and while the performance is great, I only seem to be able to get one hit per RaycastCommand. This is made worse by the fast that you can't specify queryTriggerInteraction like you can with most (all?) other physics queries so I will often need to filter out triggers. This would be fine if I could get more that one hit returned.

    Here is my test code where TestAsyncRaycastDispatch is called from MonoBehaviour.Update and TestAsyncRaycastResults is called from MonoBehaviour.LateUpdate.

    Code (CSharp):
    1. const int kNumRays = 360;
    2. const int kMaxNumHits = 10;
    3. const float kMaxDistance = 100.0f;
    4. const int kMinCommandsPerJob = 1;
    5.  
    6. private NativeArray< RaycastCommand > m_raycastCommands = new NativeArray< RaycastCommand >( kNumRays, Allocator.Persistent );
    7. private NativeArray< RaycastHit > m_raycastResults = new NativeArray< RaycastHit >( kNumRays * kMaxNumHits, Allocator.Persistent );
    8. private JobHandle m_raycastJobHandle;
    9.  
    10. private void TestAsyncRaycastDispatch()
    11. {
    12.     Vector3 origin = transform.position + Vector3.up;
    13.     Vector3 forwards = transform.forward;
    14.  
    15.     UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Build" );
    16.     for( int i = 0; i < kNumRays; ++i )
    17.     {
    18.         m_raycastCommands[ i ] = new RaycastCommand( origin, Quaternion.AngleAxis( ( i * 360.0f ) / kNumRays, Vector3.up ) * forwards, kMaxDistance, CameraManager.OpaqueLevelGeometryLayerMask, kMaxNumHits );
    19.     }
    20.     UnityEngine.Profiling.Profiler.EndSample();
    21.  
    22.     UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Schedule" );
    23.     m_raycastJobHandle = RaycastCommand.ScheduleBatch( m_raycastCommands, m_raycastResults, kMinCommandsPerJob );
    24.     JobHandle.ScheduleBatchedJobs();
    25.     UnityEngine.Profiling.Profiler.EndSample();
    26. }
    27.  
    28. private void TestAsyncRaycastResults()
    29. {
    30.     UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Complete" );
    31.     m_raycastJobHandle.Complete();
    32.     UnityEngine.Profiling.Profiler.EndSample();
    33.  
    34.     for( int i = 0; i < kNumRays; ++i )
    35.     {
    36.         Debug.DrawLine( m_raycastCommands[ i ].from, m_raycastCommands[ i ].from + ( m_raycastCommands[ i ].direction * m_raycastCommands[ i ].distance ), Color.grey );
    37.         for( int j = 0; j < kMaxNumHits; ++j )
    38.         {
    39.             RaycastHit hit = m_raycastResults[ ( i * kMaxNumHits ) + j ];
    40.             if( hit.collider != null )
    41.             {
    42.                 // Note that we are unable to specify that the job should not hit triggers, so we need to filter our results
    43.                 if( !hit.collider.isTrigger )
    44.                 {
    45.                     if( j == 0 )
    46.                     {
    47.                         Debug.DrawLine( m_raycastCommands[ i ].from, hit.point, Color.yellow );
    48.                     }
    49.                     DebugHelpers.DrawCross( hit.point, 0.2f, Color.cyan, DebugHelpers.RenderType.Scene );
    50.                 }
    51.             }
    52.             else
    53.             {
    54.                 // First null collider shows we have no more hits
    55.                 break;
    56.             }
    57.         }
    58.     }
    59. }
     
    Darkgaze likes this.
  2. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,980
    "If maxHits is larger than the actual number of results for the command the result buffer will contain some invalid results which did not hit anything. The first invalid result is identified by the collider being null. The second and later invalid results are not written to by the raycast command so their colliders are not guaranteed to be null. When iterating over the results the loop should stop when the first invalid result is found."

    - https://docs.unity3d.com/2018.2/Documentation/ScriptReference/RaycastCommand.html
     
  3. KieranDA

    KieranDA

    Joined:
    Feb 1, 2018
    Posts:
    16
    Yes, I get that, and if you look at line 40 in my code, I test for exactly that. The issue I have is that if there are multiple collisions down my ray cast, only the first one will be reported. So element 0 of the result buffer will have the first hit, then element 1 will have a null collider even though the ray goes through another valid collider.
     
  4. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,980
    Right, and based on what is written in the documentation that means it is not a valid collider (despite it seeming as such). So potentially file an issue on the bug reporter if you are convinced this is not the correct behaviour.

    I am assuming it is this: " The first invalid result is identified by the collider being null. The second and later invalid results are not written to by the raycast command so their colliders are not guaranteed to be null"

    It probably hits 1 null collider and then all other following ones get trimmed even though they are "valid".
     
  5. KieranDA

    KieranDA

    Joined:
    Feb 1, 2018
    Posts:
    16
    This is not the case. I can have a ray being cast and correctly colliding with a wall. I slide another collider between the start point of the ray and the wall where it is colliding and the first collision is correctly changed to the new collider, but there are no other collisions returned even though we just tested that the collider behind is a valid collider.

    You are correct however that I should submit a bug report. I will do that now.
     
  6. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Looks like bug planet to me.
     
    MadeFromPolygons likes this.
  7. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,980
    Definitely submit a bug report then

    EDIT: and link the issue number to a comment in this thread, it helps the developers a ton and also helps other indies work out whats what too :)
     
  8. KieranDA

    KieranDA

    Joined:
    Feb 1, 2018
    Posts:
    16

    Attached Files:

  9. KieranDA

    KieranDA

    Joined:
    Feb 1, 2018
    Posts:
    16
    The bug has been reproduced by Unity support. It has been sent to be resolved.
     
    MNNoxMortem and DrSpritz like this.
  10. Backer_Sultan

    Backer_Sultan

    Joined:
    Nov 5, 2018
    Posts:
    6
    Surprizingly it's not resolved yet!
     
  11. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    If anyone is reading: you can't have multiple hits in a batch, just one hit (the first) for each in the batch. This is normal, just the parameters are confusing.

    @yant @MelvMay awfully sorry pinging you guys way over here in the ECS forum but there really wasn't a satisfactory reply (actually it was ignored) in the fogbugz link above regarding the maxhits parameter lurking around redundant.

    Minor issue but quite confusing for programmers :)
     
  12. rz_0lento

    rz_0lento

    Joined:
    Oct 8, 2013
    Posts:
    2,361
    This actually surprises me now as I did know about the limitation before I started implementing this but it must have been communicated on blog post etc as docs aren't expressing this at all. Probably worth at least revising the doc page for this to make it more clear.
     
  13. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    I implemented (and abandoned) a batch raycast algorithm that gets all hits and stores them in a NativeMultiHashMap. It's got a huge initial overhead though - it must receive 1,000 requests for it to be more efficient than Physics.Raycast. I'll post the algorithm as a POC

    Code (CSharp):
    1. private static void MultithreadedRaycast(
    2.         CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult> io,
    3.         NativeArray<GetAllHitsCalculationRequest> requests,
    4.         int maxHits
    5.     )
    6.     {
    7.         int requestsCount = requests.Length;
    8.  
    9.         NativeList<GetAllHitsData> remainingData =
    10.             new NativeList<GetAllHitsData>(requestsCount, Allocator.TempJob);
    11.         NativeList<float3> remainingStarts =
    12.             new NativeList<float3>(requestsCount, Allocator.TempJob);
    13.         NativeList<int> remainingHits =
    14.             new NativeList<int>(requestsCount, Allocator.TempJob);
    15.  
    16.         PreInitJob preInit = new PreInitJob
    17.         {
    18.             data = remainingData,
    19.             writeCache = io.ToConcurrent(),
    20.             starts = remainingStarts,
    21.             hits = remainingHits,
    22.             requests = requests
    23.         };
    24.         preInit.Schedule().Complete();
    25.  
    26.         do
    27.         {
    28.             int remainingCount = remainingData.Length;
    29.  
    30.             NativeArray<RaycastHit> results =
    31.                 new NativeArray<RaycastHit>(remainingCount, Allocator.TempJob);
    32.             NativeArray<RaycastCommand> commands =
    33.                 new NativeArray<RaycastCommand>(remainingCount, Allocator.TempJob);
    34.  
    35.             InitRaycastJob initRaycast = new InitRaycastJob
    36.             {
    37.                 commandsWrite = commands,
    38.                 remainingData = remainingData,
    39.                 starts = remainingStarts
    40.             };
    41.             initRaycast.Schedule(remainingCount, INNER_BATCH_LOOP_COUNT).Complete();
    42.  
    43.             RaycastCommand.ScheduleBatch(commands, results, 1).Complete();
    44.  
    45.             ProcessResultsJob processResults = new ProcessResultsJob
    46.             {
    47.                 remainingData = remainingData,
    48.                 hits = results,
    49.                 hitAmount = remainingHits,
    50.                 writeCache = io.ToConcurrent(),
    51.                 starts = remainingStarts,
    52.                 maxHits = maxHits
    53.             };
    54.             processResults.Schedule().Complete();
    55.  
    56.             results.Dispose();
    57.             commands.Dispose();
    58.         } while (remainingData.Length > 0);
    59.  
    60.         remainingHits.Dispose();
    61.         remainingStarts.Dispose();
    62.         remainingData.Dispose();
    63.     }
    64.  
    65.  
    66.     [BurstCompile]
    67.     private struct PreInitJob : IJob
    68.     {
    69.         [WriteOnly]
    70.         public NativeList<GetAllHitsData> data;
    71.         [WriteOnly]
    72.         public NativeList<float3> starts;
    73.         [WriteOnly]
    74.         public NativeList<int> hits;
    75.         [WriteOnly]
    76.         public CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult>.Concurrent writeCache;
    77.         [ReadOnly]
    78.         public NativeArray<GetAllHitsCalculationRequest> requests;
    79.  
    80.  
    81.         public void Execute()
    82.         {
    83.             int requestsLength = requests.Length;
    84.             for(int i = 0; i < requestsLength; i++)
    85.             {
    86.                 var request = requests[i];
    87.                 data.Add(new GetAllHitsData
    88.                 {
    89.                     request = request
    90.                 });
    91.                 starts.Add(request.start);
    92.                 hits.Add(0);
    93.                 writeCache.Set(request, new GetAllHitsCalculationResult());
    94.             }
    95.         }
    96.     }
    97.  
    98.     [BurstCompile]
    99.     private struct InitRaycastJob : IJobParallelFor
    100.     {
    101.         [WriteOnly]
    102.         public NativeArray<RaycastCommand> commandsWrite;
    103.         [ReadOnly]
    104.         public NativeList<float3> starts;
    105.         [ReadOnly]
    106.         public NativeList<GetAllHitsData> remainingData;
    107.  
    108.  
    109.         public void Execute(int index)
    110.         {
    111.             GetAllHitsCalculationRequest request = remainingData[index].request;
    112.             float3 start = starts[index];
    113.             float3 end = request.end;
    114.             float3 direction = end - start;
    115.             float distance = direction.Magnitude();
    116.             commandsWrite[index] = new RaycastCommand(start, direction, distance, request.collisionMask);
    117.         }
    118.     }
    119.  
    120.     [BurstCompile]
    121.     private struct ProcessResultsJob : IJob
    122.     {
    123.         public NativeList<float3> starts;
    124.         public NativeArray<RaycastHit> hits;
    125.         [WriteOnly]
    126.         public CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult>.Concurrent writeCache;
    127.         public NativeList<GetAllHitsData> remainingData;
    128.         public NativeList<int> hitAmount;
    129.  
    130.         public int maxHits;
    131.  
    132.      
    133.         public void Execute()
    134.         {
    135.             for(int i = remainingData.Length - 1; i >=0; i--)
    136.             {
    137.                 RaycastHit hit = hits[i];
    138.                 if (hit.point == default(Vector3))
    139.                 {
    140.                     starts.RemoveAt(i);
    141.                     remainingData.RemoveAt(i);
    142.                     hitAmount.RemoveAt(i);
    143.                 }
    144.                 else
    145.                 {
    146.                     if(hitAmount[i] < maxHits)
    147.                     {
    148.                         GetAllHitsCalculationRequest request = remainingData[i].request;
    149.                         GetAllHitsCalculationResult result = new GetAllHitsCalculationResult
    150.                         {
    151.                             hit = hit,
    152.                             point = hit.point
    153.                         };
    154.                         writeCache.Set(request, result);
    155.                         hitAmount[i]++;
    156.                         starts[i] = new float3(hits[i].point) + ((request.end - starts[i]).Normalize() * 0.1f);
    157.                     }
    158.                     else
    159.                     {
    160.                         starts.RemoveAt(i);
    161.                         remainingData.RemoveAt(i);
    162.                         hitAmount.RemoveAt(i);
    163.                     }
    164.                 }
    165.             }
    166.         }
    167.     }
    168.  
     
    MNNoxMortem likes this.
  14. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
  15. Epineurien

    Epineurien

    Joined:
    Mar 9, 2013
    Posts:
    45
    Any update/ETA on this bug ?
    Will RaycastCommands stay forever like this ?
    Or will it be replaced by another system ?
     
  16. cerea1

    cerea1

    Joined:
    Aug 15, 2019
    Posts:
    2
    OmnifunGames likes this.
  17. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    CPlusSharp22 likes this.
  18. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    397
    This is outrageous. Still in 2019.4, and the rest of the versions (until 2021). My whole asset development destroyed due to this maxhits assumption. I need to support 2018 and 2019 too.

    Why is the bug closed??
    https://fogbugz.unity3d.com/default.asp?1041352_j87fk47jv1pg5nfe

    I'll find another one or open a new one.
     
    Last edited: Jul 26, 2021
  19. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    397
    Last edited: Jul 26, 2021
  20. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    397
    Ok. Apart from the code from @cerea1 (thanks a lot!!) I ended up rewriting it again to make it similar to the RaycastCommand.ScheduleBatch method. But this time using much less memory, and improved efficiency, using just one job and reusing data structs.

    It's as easy as adding the script to your project and calling:
    RaycastCommandMultihit.ScheduleBatch(...) and it will return the correct results.

    I created a gumroad pack as I generally do for these kind of complicated stuff that I need to upload.
    You can download the script and use it freely. All the information is here:

    https://lidiamartinez.gumroad.com/l/wKyFk
     
    apkdev and hippocoder like this.
  21. vx4

    vx4

    Joined:
    Dec 11, 2012
    Posts:
    181
    There some good news,
    Add RaycastHit.colliderInstanceID
    so now you can check from job if there hit instead rely on RaycastHit.collider==null
     
    Krajca likes this.
  22. vx4

    vx4

    Joined:
    Dec 11, 2012
    Posts:
    181
    Another news form Unity 2022.1.0 Alpha 7 Release notes

    Physics: Updated RaycastCommand's Scripting API to mention that it won't return more than one hit.
     
  23. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    397
    Thank lord... they didn't fix it, but make it clear...at least... haha!
    Uh, that's neat. Sadly I have to do it anyway for older versions. But it's cool, thanks!
     
    Pr0x1d likes this.
  24. MariaKhomenko

    MariaKhomenko

    Joined:
    Dec 13, 2019
    Posts:
    43
    They fixed it in 2022.2, just tested it!
     
    Darkgaze and RaL like this.
  25. Darkgaze

    Darkgaze

    Joined:
    Apr 3, 2017
    Posts:
    397
    Thank god... I'll have to support it in my asset, anyway, but glad it was fixed. Thanks.