Problems using RaycastCommand together with Jobs and some suggestions to it

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:

_raycastResults = new NativeArray<RaycastHit>(size, allocationType).
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

Found this thread from 2 years ago which is closely connected to my problem .
They also have the problem of the bottleneck after getting all the raycastHits and not being able to access .collider inside of them.

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

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

//somewhere in Awake, because I pre calculate vaues in Commands array and length always the same for Commands and results

Results = new NativeArray<RaycastHit>(PrecalculatedWindRays.Length, Allocator.Persistent);
Commands = new NativeArray<RaycastCommand>(PrecalculatedWindRays.Length, Allocator.Persistent);

//then

private void FixedUpdate ()
{
   RaycastCommand.ScheduleBatch(Commands, Results, 1).Complete();
   NativeList<RaycastHit> validRaycastHits = new NativeList<RaycastHit>(Results.Length, Allocator.TempJob);
   NativeList<RaycastHit>.ParallelWriter validRaycastHitsWriter = validRaycastHits.AsParallelWriter();
  
   new HitProceedJob
   {
      RaycastHitDataColection = Results,
      ValidRaycastHits = validRaycastHitsWriter
   }.Schedule(Results.Length, 32).Complete();

   for (int hitObjectPointer = 0; hitObjectPointer < validRaycastHits.Length; hitObjectPointer++)
   {
      RaycastHit batchedHit = validRaycastHits[hitObjectPointer];
      batchedHit.rigidbody.AddForceAtPosition(Vector3.forward * 0.001f, batchedHit.point, ForceMode.Impulse);
   }
  
   validRaycastHits.Dispose();
}

[BurstCompile]
private struct HitProceedJob : IJobParallelFor
{
   [ReadOnly]
   public NativeArray<RaycastHit> RaycastHitDataColection;
   [WriteOnly]
   public NativeList<RaycastHit>.ParallelWriter ValidRaycastHits;
  
   public void Execute (int index)
   {
      RaycastHit currentRaycast = RaycastHitDataColection[index];
     
      if (currentRaycast.point != default) //it's a HACK!!! Original and right solution is currentRaycast.Collider != null, but this doesn't work in a job.
      {
         ValidRaycastHits.AddNoResize(currentRaycast);
      }
   }
}

//and dispose arrays

private void OnDestroy ()
{
   Results.Dispose();
   Commands.Dispose();
}

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

1 Like

Thank you for your feedback. Your way really looks like black magic but seems it works as expected + eliminates invalid coordinates comparison.

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

1 Like

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.

So, the original code

        [StructLayout(LayoutKind.Sequential)]
        private struct RaycastHitPublic
        {
            public Vector3 m_Point;
            public Vector3 m_Normal;
            public int m_FaceID;
            public float m_Distance;
            public Vector2 m_UV;
            public int m_ColliderID;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe int GetColliderID (RaycastHit hit)
        {
            RaycastHitPublic h = *(RaycastHitPublic*) &hit;
            return h.m_ColliderID;
        }

and short one

        [StructLayout(LayoutKind.Sequential)]
        private struct RaycastHitPublic
        {
            public int m_ColliderID;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe int GetColliderID (RaycastHit hit)
        {
            RaycastHitPublic h = *(RaycastHitPublic*) &hit;
            return h.m_ColliderID;
        }

Due to my observation that short code works as expected (maybe a can do some unit test to be sure on 100%)

Btw, I found that UnsafeUtility has something like that

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int GetColliderID (RaycastHit hit)
        {
            return UnsafeUtility.As<RaycastHit, RaycastHitPublic>(ref hit).m_ColliderID;
        }

Seems works exactly as your code

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?

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.

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

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) )

I guess it means the change in 2021.1 where you can access RaycastHit.colliderInstanceID

3 Likes

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!

2 Likes

Whoa, this is really nice!

Any chance this gets backported to 2019.4?

Features never get backported, only bug fixes.

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

public void Execute(ParticleSystemJobData particles)
        {
            var n = 0;
            var drop = new Droplet();
            var life = particles.aliveTimePercent;
            var color = particles.startColors;
            for (int i = startIndex; i < endIndex; i++) {
                var hit = rcHit[i];
                if (hit.colliderInstanceID == colliderID)
                {
                    life[i] = 100.0f;
                    drop.texCoord = hit.textureCoord;
                    drop.color = color[i];
                    droplets[n++] = drop;
                }
            }
            dropletsCount.Value = n;
        }

Error message I get is:

I am able to suppress warning by using. However the results just contains zeros!

[StructLayout(LayoutKind.Sequential)]
        internal struct RaycastHitPublic
        {
            public Vector3 m_Point;
            public Vector3 m_Normal;
            public int m_FaceID;
            public float m_Distance;
            public Vector2 m_UV;
            public int m_ColliderID;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int GetColliderID(RaycastHit hit)
        {
            unsafe
            {
                RaycastHitPublic h = *(RaycastHitPublic*)&hit;
                return h.m_ColliderID;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Vector2 GetTextureCoord(RaycastHit hit)
        {
            unsafe
            {
                RaycastHitPublic h = *(RaycastHitPublic*)&hit;
                return h.m_UV;
            }
        }

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.