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

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.

const int kNumRays = 360;
const int kMaxNumHits = 10;
const float kMaxDistance = 100.0f;
const int kMinCommandsPerJob = 1;

private NativeArray< RaycastCommand > m_raycastCommands = new NativeArray< RaycastCommand >( kNumRays, Allocator.Persistent );
private NativeArray< RaycastHit > m_raycastResults = new NativeArray< RaycastHit >( kNumRays * kMaxNumHits, Allocator.Persistent );
private JobHandle m_raycastJobHandle;

private void TestAsyncRaycastDispatch()
{
    Vector3 origin = transform.position + Vector3.up;
    Vector3 forwards = transform.forward;

    UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Build" );
    for( int i = 0; i < kNumRays; ++i )
    {
        m_raycastCommands[ i ] = new RaycastCommand( origin, Quaternion.AngleAxis( ( i * 360.0f ) / kNumRays, Vector3.up ) * forwards, kMaxDistance, CameraManager.OpaqueLevelGeometryLayerMask, kMaxNumHits );
    }
    UnityEngine.Profiling.Profiler.EndSample();

    UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Schedule" );
    m_raycastJobHandle = RaycastCommand.ScheduleBatch( m_raycastCommands, m_raycastResults, kMinCommandsPerJob );
    JobHandle.ScheduleBatchedJobs();
    UnityEngine.Profiling.Profiler.EndSample();
}

private void TestAsyncRaycastResults()
{
    UnityEngine.Profiling.Profiler.BeginSample( "AsyncRaycast.Complete" );
    m_raycastJobHandle.Complete();
    UnityEngine.Profiling.Profiler.EndSample();

    for( int i = 0; i < kNumRays; ++i )
    {
        Debug.DrawLine( m_raycastCommands[ i ].from, m_raycastCommands[ i ].from + ( m_raycastCommands[ i ].direction * m_raycastCommands[ i ].distance ), Color.grey );
        for( int j = 0; j < kMaxNumHits; ++j )
        {
            RaycastHit hit = m_raycastResults[ ( i * kMaxNumHits ) + j ];
            if( hit.collider != null )
            {
                // Note that we are unable to specify that the job should not hit triggers, so we need to filter our results
                if( !hit.collider.isTrigger )
                {
                    if( j == 0 )
                    {
                        Debug.DrawLine( m_raycastCommands[ i ].from, hit.point, Color.yellow );
                    }
                    DebugHelpers.DrawCross( hit.point, 0.2f, Color.cyan, DebugHelpers.RenderType.Scene );
                }
            }
            else
            {
                // First null collider shows we have no more hits
                break;
            }
        }
    }
}
1 Like

“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.”

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.

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”.

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.

3 Likes

Looks like bug planet to me.

1 Like

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 :slight_smile:

Bug report submitted: Case 1041352.

Fogbugz: https://fogbugz.unity3d.com/default.asp?1041352_j87fk47jv1pg5nfe

Also I have attached to this post the minimal reproduction project that I attached to the bug report in case it’s useful for anyone on here.

3507526–279876–AsyncRaycastBug.zip (552 KB)

The bug has been reproduced by Unity support. It has been sent to be resolved.

2 Likes

Surprizingly it’s not resolved yet!

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 :slight_smile:

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.

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

private static void MultithreadedRaycast(
        CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult> io,
        NativeArray<GetAllHitsCalculationRequest> requests,
        int maxHits
    )
    {
        int requestsCount = requests.Length;

        NativeList<GetAllHitsData> remainingData =
            new NativeList<GetAllHitsData>(requestsCount, Allocator.TempJob);
        NativeList<float3> remainingStarts =
            new NativeList<float3>(requestsCount, Allocator.TempJob);
        NativeList<int> remainingHits =
            new NativeList<int>(requestsCount, Allocator.TempJob);

        PreInitJob preInit = new PreInitJob
        {
            data = remainingData,
            writeCache = io.ToConcurrent(),
            starts = remainingStarts,
            hits = remainingHits,
            requests = requests
        };
        preInit.Schedule().Complete();

        do
        {
            int remainingCount = remainingData.Length;

            NativeArray<RaycastHit> results =
                new NativeArray<RaycastHit>(remainingCount, Allocator.TempJob);
            NativeArray<RaycastCommand> commands =
                new NativeArray<RaycastCommand>(remainingCount, Allocator.TempJob);

            InitRaycastJob initRaycast = new InitRaycastJob
            {
                commandsWrite = commands,
                remainingData = remainingData,
                starts = remainingStarts
            };
            initRaycast.Schedule(remainingCount, INNER_BATCH_LOOP_COUNT).Complete();

            RaycastCommand.ScheduleBatch(commands, results, 1).Complete();

            ProcessResultsJob processResults = new ProcessResultsJob
            {
                remainingData = remainingData,
                hits = results,
                hitAmount = remainingHits,
                writeCache = io.ToConcurrent(),
                starts = remainingStarts,
                maxHits = maxHits
            };
            processResults.Schedule().Complete();

            results.Dispose();
            commands.Dispose();
        } while (remainingData.Length > 0);

        remainingHits.Dispose();
        remainingStarts.Dispose();
        remainingData.Dispose();
    }


    [BurstCompile]
    private struct PreInitJob : IJob
    {
        [WriteOnly]
        public NativeList<GetAllHitsData> data;
        [WriteOnly]
        public NativeList<float3> starts;
        [WriteOnly]
        public NativeList<int> hits;
        [WriteOnly]
        public CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult>.Concurrent writeCache;
        [ReadOnly]
        public NativeArray<GetAllHitsCalculationRequest> requests;


        public void Execute()
        {
            int requestsLength = requests.Length;
            for(int i = 0; i < requestsLength; i++)
            {
                var request = requests[i];
                data.Add(new GetAllHitsData
                {
                    request = request
                });
                starts.Add(request.start);
                hits.Add(0);
                writeCache.Set(request, new GetAllHitsCalculationResult());
            }
        }
    }

    [BurstCompile]
    private struct InitRaycastJob : IJobParallelFor
    {
        [WriteOnly]
        public NativeArray<RaycastCommand> commandsWrite;
        [ReadOnly]
        public NativeList<float3> starts;
        [ReadOnly]
        public NativeList<GetAllHitsData> remainingData;


        public void Execute(int index)
        {
            GetAllHitsCalculationRequest request = remainingData[index].request;
            float3 start = starts[index];
            float3 end = request.end;
            float3 direction = end - start;
            float distance = direction.Magnitude();
            commandsWrite[index] = new RaycastCommand(start, direction, distance, request.collisionMask);
        }
    }

    [BurstCompile]
    private struct ProcessResultsJob : IJob
    {
        public NativeList<float3> starts;
        public NativeArray<RaycastHit> hits;
        [WriteOnly]
        public CalculationIO<GetAllHitsCalculationRequest, GetAllHitsCalculationResult>.Concurrent writeCache;
        public NativeList<GetAllHitsData> remainingData;
        public NativeList<int> hitAmount;

        public int maxHits;

      
        public void Execute()
        {
            for(int i = remainingData.Length - 1; i >=0; i--)
            {
                RaycastHit hit = hits[i];
                if (hit.point == default(Vector3))
                {
                    starts.RemoveAt(i);
                    remainingData.RemoveAt(i);
                    hitAmount.RemoveAt(i);
                }
                else
                {
                    if(hitAmount[i] < maxHits)
                    {
                        GetAllHitsCalculationRequest request = remainingData[i].request;
                        GetAllHitsCalculationResult result = new GetAllHitsCalculationResult
                        {
                            hit = hit,
                            point = hit.point
                        };
                        writeCache.Set(request, result);
                        hitAmount[i]++;
                        starts[i] = new float3(hits[i].point) + ((request.end - starts[i]).Normalize() * 0.1f);
                    }
                    else
                    {
                        starts.RemoveAt(i);
                        remainingData.RemoveAt(i);
                        hitAmount.RemoveAt(i);
                    }
                }
            }
        }
    }
1 Like

Will the Unity team ever attempt to make RaycastCommand detect multiple hits? I understand that batched query support is deprecated (Scene Queries — NVIDIA PhysX SDK 3.4.2 Documentation) but it may still exist in PhysX, and a multiple hit-detecting RaycastCommand is very useful

Any update/ETA on this bug ?
Will RaycastCommands stay forever like this ?
Or will it be replaced by another system ?

Here’s a little workaround GitHub - cerea1/RaycastJobs: Workaround for jobified raycasts limitations

1 Like

This is still an issue and the documentation has not been updated. Rays fired by RaycastCommand will only ever return 1 hit. Here’s my thread about it: How do I parse multiple hits from the results of RaycastCommand.ScheduleBatch()?

1 Like

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.

Well. It is closed by design. Absolutely outrageous.

And still available in the documentation and the API…

1 Like

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:

2 Likes