Unity Raycast and highlighting mesh triangle of intersection

I would like to fire a ray, and highlight the triangle of the point on a mesh where the collision occurs.

I have the following code, which runs when I left mouse click, myEyes is my FPS camera.:

void Dig()
{
    Ray ray = new Ray(myEyes.transform.position, myEyes.transform.forward);
    Debug.DrawRay(ray.origin, ray.direction * 50f, Color.cyan, 2000f) ;
    RaycastHit hit;


    // if(Physics.Raycast(ray, out hit,1000f, LayerMask.NameToLayer("Ground"))) -- this dos not work - debug line shows ray is hitting layer 8 == ground but with the mask, nothing is hit?
    if (Physics.Raycast(ray,out hit, 50f))
    {

        //remove a 'spades' worth of voxels, if pointing at the terrain, and shove them somewhere random nearby.
        Debug.Log("Tag is:" + hit.collider.gameObject.tag + " and object name is:" + hit.collider.gameObject.name + "And layer is:" + hit.collider.gameObject.layer);


        MeshCollider meshCollider = hit.collider as MeshCollider;
        if (meshCollider == null || meshCollider.sharedMesh == null)
            return;

       
        Mesh mesh = meshCollider.sharedMesh;
        Vector3[] vertices = mesh.vertices;
        int[] triangles = mesh.triangles;
        Vector3 p0 = vertices[triangles[hit.triangleIndex * 3 + 0]];
        Vector3 p1 = vertices[triangles[hit.triangleIndex * 3 + 1]];
        Vector3 p2 = vertices[triangles[hit.triangleIndex * 3 + 2]];
        Transform hitTransform = hit.collider.transform;
        p0 = hitTransform.TransformPoint(p0);
        p1 = hitTransform.TransformPoint(p1);
        p2 = hitTransform.TransformPoint(p2);
        Debug.DrawLine(p0, p1,Color.red,1000f);
        Debug.DrawLine(p1, p2, Color.red, 1000f);
        Debug.DrawLine(p2, p0, Color.red, 1000f);


    }
    else{
        Debug.Log("Hit nothing");
    }
}

The outcome of this is the following:

Note that triangles are indeed highlighted - but they are nowhere near the collision point? Can somebody highlight why.

Also, the Debug output from this example is :

Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)
Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)
Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)
Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)
Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)
Tag is:Terrain and object name is:Chunk (0, 0, 0)And layer is:8
UnityEngine.Debug:Log(Object)

My “Ground” layer is layer 8 - and when I try a Raycast with a layerMask - it fails to hit anything at all, I have no idea why. This line casts the ray with such a mask, and causes the if condition to fail, resulting in it dropping into the “Hit Nothing” debug output line.

Any help would be very much appreciated.

The script in general seems to be working.
Tested it with a default plane and the myEyes set to the main camera.

Is it therefore something to do with my mesh?!

You could try it with a Default plane and if that is working fine in your project its probably the mesh.
is that terrain object scaled?

This is wrong:

if(Physics.Raycast(ray, out hit,1000f, LayerMask.NameToLayer("Ground")))

You need a layermask that targets the ground layer, while NameToLayer gives you that layer’s index, which is not the same thing.

So if ground is layer 8, LayerMask.NameToLayer gives 8, while the mask you want is 1 << 8, which is 0b100000000 or 256.

You should read up on bitmasks if that’s confusing! Or check the layermask docs. For a simpler solution, simply use a public LayerMask field, and use that as your layermask.

2 Likes

Great - got it thanks @Baste i’ll read up on this. - I changed it to int layerMaskGround = 1 << LayerMask.NameToLayer(“Ground”) and it works now, despite me not fully understanding it - will read the docs as you suggest.

This is my MeshGenerator class - can you take a look and maybe help see why this strange behaviour occurs when selecting the triangle where the collision occurs?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class MeshGenerator : MonoBehaviour
{

    const int threadGroupSize = 8;

    [Header("General Settings")]
    public DensityGenerator densityGenerator;

    public bool fixedMapSize;
    [ConditionalHide(nameof(fixedMapSize), true)]
    public Vector3Int numChunks = Vector3Int.one;
    [ConditionalHide(nameof(fixedMapSize), false)]
    public Transform viewer;
    [ConditionalHide(nameof(fixedMapSize), false)]
    public float viewDistance = 30;

    [Space()]
    public bool autoUpdateInEditor = true;
    public bool autoUpdateInGame = true;
    public ComputeShader shader;
    public Material mat;
    public bool generateColliders;

    [Header("Voxel Settings")]
    public float isoLevel;
    public float boundsSize = 1;
    public Vector3 offset = Vector3.zero;

    [Range(2, 100)]
    public int numPointsPerAxis = 30;

    [Header("Gizmos")]
    public bool showBoundsGizmo = true;
    public Color boundsGizmoCol = Color.white;

    GameObject chunkHolder;
    const string chunkHolderName = "Chunks Holder";
    List<Chunk> chunks;
    Dictionary<Vector3Int, Chunk> existingChunks;
    Queue<Chunk> recycleableChunks;

    // Buffers
    ComputeBuffer triangleBuffer;
    ComputeBuffer pointsBuffer;
    ComputeBuffer triCountBuffer;

    bool settingsUpdated;

    void Awake()
    {
        if (Application.isPlaying && !fixedMapSize)
        {
            InitVariableChunkStructures();

            // var oldChunks = FindObjectsOfType<Chunk> ();
            // for (int i = oldChunks.Length - 1; i >= 0; i--) {
            //     Destroy (oldChunks[i].gameObject);
            //  }
        }
    }

    void Update()
    {
        // Update endless terrain
        if ((Application.isPlaying && !fixedMapSize))
        {
            Debug.Log("In normal application update path");
            Run();
        }

        if (settingsUpdated)
        {
            Debug.Log("In settings updated");
            RequestMeshUpdate();
            settingsUpdated = false;
        }
    }

    public void Run()
    {
        CreateBuffers();

        if (fixedMapSize)
        {
            InitChunks();
            UpdateAllChunks();

        }
        else
        {
            if (Application.isPlaying)
            {
                InitVisibleChunks();
            }
        }

        // Release buffers immediately in editor
        if (!Application.isPlaying)
        {
            ReleaseBuffers();
        }

    }

    public void RequestMeshUpdate()
    {
        if ((Application.isPlaying && autoUpdateInGame) || (!Application.isPlaying && autoUpdateInEditor))
        {
            Run();
        }
    }

    void InitVariableChunkStructures()
    {
        recycleableChunks = new Queue<Chunk>();
        chunks = new List<Chunk>();
        existingChunks = new Dictionary<Vector3Int, Chunk>();
    }

    void InitVisibleChunks()
    {
        if (chunks == null)
        {
            return;
        }
        CreateChunkHolder();

        Vector3 p = viewer.position;
        Vector3 ps = p / boundsSize;
        Vector3Int viewerCoord = new Vector3Int(Mathf.RoundToInt(ps.x), Mathf.RoundToInt(ps.y), Mathf.RoundToInt(ps.z));

        int maxChunksInView = Mathf.CeilToInt(viewDistance / boundsSize);
        float sqrViewDistance = viewDistance * viewDistance;

        // Go through all existing chunks and flag for recyling if outside of max view dst
        for (int i = chunks.Count - 1; i >= 0; i--)
        {
            Chunk chunk = chunks[i];
            Vector3 centre = CentreFromCoord(chunk.coord);
            Vector3 viewerOffset = p - centre;
            Vector3 o = new Vector3(Mathf.Abs(viewerOffset.x), Mathf.Abs(viewerOffset.y), Mathf.Abs(viewerOffset.z)) - Vector3.one * boundsSize / 2;
            float sqrDst = new Vector3(Mathf.Max(o.x, 0), Mathf.Max(o.y, 0), Mathf.Max(o.z, 0)).sqrMagnitude;
            if (sqrDst > sqrViewDistance)
            {
                existingChunks.Remove(chunk.coord);
                recycleableChunks.Enqueue(chunk);
                chunks.RemoveAt(i);
            }
        }

        for (int x = -maxChunksInView; x <= maxChunksInView; x++)
        {
            for (int y = -maxChunksInView; y <= maxChunksInView; y++)
            {
                for (int z = -maxChunksInView; z <= maxChunksInView; z++)
                {
                    Vector3Int coord = new Vector3Int(x, y, z) + viewerCoord;

                    if (existingChunks.ContainsKey(coord))
                    {
                        continue;
                    }

                    Vector3 centre = CentreFromCoord(coord);
                    Vector3 viewerOffset = p - centre;
                    Vector3 o = new Vector3(Mathf.Abs(viewerOffset.x), Mathf.Abs(viewerOffset.y), Mathf.Abs(viewerOffset.z)) - Vector3.one * boundsSize / 2;
                    float sqrDst = new Vector3(Mathf.Max(o.x, 0), Mathf.Max(o.y, 0), Mathf.Max(o.z, 0)).sqrMagnitude;

                    // Chunk is within view distance and should be created (if it doesn't already exist)
                    if (sqrDst <= sqrViewDistance)
                    {

                        Bounds bounds = new Bounds(CentreFromCoord(coord), Vector3.one * boundsSize);
                        if (IsVisibleFrom(bounds, Camera.main))
                        {
                            if (recycleableChunks.Count > 0)
                            {
                                Chunk chunk = recycleableChunks.Dequeue();
                                chunk.coord = coord;
                                existingChunks.Add(coord, chunk);
                                chunks.Add(chunk);
                                UpdateChunkMesh(chunk);
                            }
                            else
                            {
                                Chunk chunk = CreateChunk(coord);
                                chunk.coord = coord;
                                chunk.SetUp(mat, generateColliders);
                                existingChunks.Add(coord, chunk);
                                chunks.Add(chunk);
                                UpdateChunkMesh(chunk);
                            }
                        }
                    }

                }
            }
        }
    }

    public bool IsVisibleFrom(Bounds bounds, Camera camera)
    {
        Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera);
        return GeometryUtility.TestPlanesAABB(planes, bounds);
    }

    public void UpdateChunkMesh(Chunk chunk)
    {
        int numVoxelsPerAxis = numPointsPerAxis - 1;
        int numThreadsPerAxis = Mathf.CeilToInt(numVoxelsPerAxis / (float)threadGroupSize);
        float pointSpacing = boundsSize / (numPointsPerAxis - 1);

        Vector3Int coord = chunk.coord;
        Vector3 centre = CentreFromCoord(coord);

        Vector3 worldBounds = new Vector3(numChunks.x, numChunks.y, numChunks.z) * boundsSize;

        densityGenerator.Generate(pointsBuffer, numPointsPerAxis, boundsSize, worldBounds, centre, offset, pointSpacing);

        triangleBuffer.SetCounterValue(0);
        shader.SetBuffer(0, "points", pointsBuffer);
        shader.SetBuffer(0, "triangles", triangleBuffer);
        shader.SetInt("numPointsPerAxis", numPointsPerAxis);
        shader.SetFloat("isoLevel", isoLevel);

        shader.Dispatch(0, numThreadsPerAxis, numThreadsPerAxis, numThreadsPerAxis);

        // Get number of triangles in the triangle buffer
        ComputeBuffer.CopyCount(triangleBuffer, triCountBuffer, 0);
        int[] triCountArray = { 0 };
        triCountBuffer.GetData(triCountArray);
        int numTris = triCountArray[0];

        // Get triangle data from shader
        Triangle[] tris = new Triangle[numTris];
        triangleBuffer.GetData(tris, 0, 0, numTris);

        Mesh mesh = chunk.mesh;
        mesh.Clear();

        var vertices = new Vector3[numTris * 3];
        var meshTriangles = new int[numTris * 3];

        for (int i = 0; i < numTris; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                meshTriangles[i * 3 + j] = i * 3 + j;
                vertices[i * 3 + j] = tris[i][j];
            }
        }
        mesh.vertices = vertices;
        mesh.triangles = meshTriangles;

        mesh.RecalculateNormals();
    }

    public void UpdateAllChunks()
    {

        // Create mesh for each chunk
        foreach (Chunk chunk in chunks)
        {
            UpdateChunkMesh(chunk);
        }

    }

    void OnDestroy()
    {
        if (Application.isPlaying)
        {
            ReleaseBuffers();
        }
    }

    void CreateBuffers()
    {
        int numPoints = numPointsPerAxis * numPointsPerAxis * numPointsPerAxis;
        int numVoxelsPerAxis = numPointsPerAxis - 1;
        int numVoxels = numVoxelsPerAxis * numVoxelsPerAxis * numVoxelsPerAxis;
        int maxTriangleCount = numVoxels * 5;

        // Always create buffers in editor (since buffers are released immediately to prevent memory leak)
        // Otherwise, only create if null or if size has changed
        if (!Application.isPlaying || (pointsBuffer == null || numPoints != pointsBuffer.count))
        {
            if (Application.isPlaying)
            {
                ReleaseBuffers();
            }
            triangleBuffer = new ComputeBuffer(maxTriangleCount, sizeof(float) * 3 * 3, ComputeBufferType.Append);
            pointsBuffer = new ComputeBuffer(numPoints, sizeof(float) * 4);
            triCountBuffer = new ComputeBuffer(1, sizeof(int), ComputeBufferType.Raw);

        }
    }

    void ReleaseBuffers()
    {
        if (triangleBuffer != null)
        {
            triangleBuffer.Release();
            pointsBuffer.Release();
            triCountBuffer.Release();
        }
    }

    Vector3 CentreFromCoord(Vector3Int coord)
    {
        // Centre entire map at origin -- gives 0,0,0 as response for 1 chunk
        if (fixedMapSize)
        {
            Vector3 totalBounds = (Vector3)numChunks * boundsSize;
            return -totalBounds / 2 + (Vector3)coord * boundsSize + Vector3.one * boundsSize / 2;
        }

        return new Vector3(coord.x, coord.y, coord.z) * boundsSize;
    }

    void CreateChunkHolder()
    {
        // Create/find mesh holder object for organizing chunks under in the hierarchy
        if (chunkHolder == null)
        {
            if (GameObject.Find(chunkHolderName))
            {
                chunkHolder = GameObject.Find(chunkHolderName);
            }
            else
            {
                chunkHolder = new GameObject(chunkHolderName);
            }
        }
    }

    // Create/get references to all chunks
    void InitChunks()
    {
        CreateChunkHolder();
        chunks = new List<Chunk>();
        List<Chunk> oldChunks = new List<Chunk>(FindObjectsOfType<Chunk>());
     
        // Go through all coords and create a chunk there if one doesn't already exist
        for (int x = 0; x < numChunks.x; x++)
        {
            for (int y = 0; y < numChunks.y; y++)
            {
                for (int z = 0; z < numChunks.z; z++)
                {
                    Vector3Int coord = new Vector3Int(x, y, z);
                    bool chunkAlreadyExists = false;

                    // If chunk already exists, add it to the chunks list, and remove from the old list.
                    for (int i = 0; i < oldChunks.Count; i++)
                    {
                        if (oldChunks[i].coord == coord)
                        {
                            chunks.Add(oldChunks[i]);
                            oldChunks.RemoveAt(i);
                            chunkAlreadyExists = true;
                            break;
                        }
                    }

                    // Create new chunk
                    if (!chunkAlreadyExists)
                    {
                        var newChunk = CreateChunk(coord);
                        chunks.Add(newChunk);
                    }

                    chunks[chunks.Count - 1].SetUp(mat, generateColliders);
                }
            }
        }

        // Delete all unused chunks
        for (int i = 0; i < oldChunks.Count; i++)
        {
            oldChunks[i].DestroyOrDisable();
        }
    }

    Chunk CreateChunk(Vector3Int coord)
    {
        GameObject chunk = new GameObject($"Chunk ({coord.x}, {coord.y}, {coord.z})");
        chunk.transform.parent = chunkHolder.transform;
        Chunk newChunk = chunk.AddComponent<Chunk>();
        newChunk.coord = coord;
        return newChunk;
    }

    void OnValidate()
    {
        settingsUpdated = true;
    }

    struct Triangle
    {
#pragma warning disable 649 // disable unassigned variable warning
        public Vector3 a;
        public Vector3 b;
        public Vector3 c;

        public Vector3 this[int i]
        {
            get
            {
                switch (i)
                {
                    case 0:
                        return a;
                    case 1:
                        return b;
                    default:
                        return c;
                }
            }
        }
    }

    void OnDrawGizmos()
    {
        if (showBoundsGizmo)
        {
            Gizmos.color = boundsGizmoCol;

            List<Chunk> chunks = (this.chunks == null) ? new List<Chunk>(FindObjectsOfType<Chunk>()) : this.chunks;
            foreach (var chunk in chunks)
            {
                Bounds bounds = new Bounds(CentreFromCoord(chunk.coord), Vector3.one * boundsSize);
                Gizmos.color = boundsGizmoCol;
                Gizmos.DrawWireCube(CentreFromCoord(chunk.coord), Vector3.one * boundsSize);
            }
        }
    }

}

That’s a lot of code! Hard to know where to start.

@Baste agreed - apologies.If we look back at this dig function, this is the bit where i try to determine which triangle was hit by the collider :

MeshCollider meshCollider = hit.collider as MeshCollider;
            if (meshCollider == null || meshCollider.sharedMesh == null)
                return;

          
            Mesh mesh = meshCollider.sharedMesh;
            Vector3[] vertices = mesh.vertices;
            int[] triangles = mesh.triangles;
            Vector3 p0 = vertices[triangles[hit.triangleIndex * 3 + 0]];
            Vector3 p1 = vertices[triangles[hit.triangleIndex * 3 + 1]];
            Vector3 p2 = vertices[triangles[hit.triangleIndex * 3 + 2]];
            Transform hitTransform = hit.collider.transform;

It grabs the hit collider as a meshCollider - gets the shared mesh.

Gets that mesh vertices, gets the triangles, and then uses the hit.triangleIndex to find the 3 points that have been hit. I assume this is where the problem is - probably in my logic I am doing something wrong?

The sharedMesh that is returned, is equal to the mesh which is generated in the MeshGenerator class I pasted in the previous post for context :slight_smile:

Dig() looks fine, tbh. The only issue is that you’re drawing the lines for 1000 seconds, so it might just be that you’re looking at old results.

To debug this properly; first I’d copy p0, p1, and p2 (after you transform them) to fields, and draw them in OnDrawGizmos with Gizmos.DrawLine, at least until you’ve got it right. Then you’ll only be looking at a single triangle at a time.

After that, I’d reduce the mesh to only a single triangle, and then keep adding them one at a time. In fact, run your algorithm on a normal quad, see if you get it working there first. Running it on top of the generated mesh makes it hard to tell where the issue lies.

1 Like

Okay @Baste - good idea.

So I added a quad, rotated it by 90 degrees on the x axis so i could stand on it.

Clicked, boom - it highlights the triangle I would expect! Great!

My updated code is as you requested - moved the draw line into Gizmos(not sure why that is an important step but regardless it works) new code here:

Vector3 p0, p1, p2;
    Boolean drawGizmosReady = false;
    void Dig()
    {
        Ray ray = new Ray(myEyes.transform.position, myEyes.transform.forward);
        Debug.DrawRay(ray.origin, ray.direction * 50f, Color.cyan, 20f) ;
        RaycastHit hit;

        int layerMaskGround = 1 << LayerMask.NameToLayer("Ground");
         if(Physics.Raycast(ray, out hit,1000f, layerMaskGround)) //-- this dos not work - debug line shows ray is hitting layer 8 == ground but with the mask, nothing is hit?
        //if (Physics.Raycast(ray,out hit, 50f))
        {

            //remove a 'spades' worth of voxels, if pointing at the terrain, and shove them somewhere random nearby.
            Debug.Log("Tag is:" + hit.collider.gameObject.tag + " and object name is:" + hit.collider.gameObject.name + "And layer is:" + hit.collider.gameObject.layer);


            MeshCollider meshCollider = hit.collider as MeshCollider;
            if (meshCollider == null || meshCollider.sharedMesh == null)
                return;

           
            Mesh mesh = meshCollider.sharedMesh;
            Vector3[] vertices = mesh.vertices;
            int[] triangles = mesh.triangles;
            drawGizmosReady = false;
            p0 = vertices[triangles[hit.triangleIndex * 3 + 0]];
            p1 = vertices[triangles[hit.triangleIndex * 3 + 1]];
            p2 = vertices[triangles[hit.triangleIndex * 3 + 2]];
            Transform hitTransform = hit.collider.transform;
          
            p0 = hitTransform.TransformPoint(p0);
            p1 = hitTransform.TransformPoint(p1);
            p2 = hitTransform.TransformPoint(p2);
            drawGizmosReady = true;
           // Debug.DrawLine(p0, p1,Color.red,1000f);
           // Debug.DrawLine(p1, p2, Color.red, 1000f);
           // Debug.DrawLine(p2, p0, Color.red, 1000f);


        }
        else{
            Debug.Log("Hit nothing");
        }
    }


    void OnDrawGizmos()
    {
        // Draw a yellow sphere at the transform's position

        if (drawGizmosReady)
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(p0, p1);
            Gizmos.DrawLine(p1, p2);
            Gizmos.DrawLine(p2, p0);
            Gizmos.DrawSphere(transform.position, 1);
        }
    }

So you now suggest running the algorithm to generate my mesh with only one triangle, and work on it from there. I like the idea, but looking at my mesh generator, im not sure how to restrict it to 1 triangle? I used the MeshGenerator code from somebody’s tutorial and it worked (up until now so I dont understand it intricately).

To me I think I would limit something in the MeshGenerator around :

 densityGenerator.Generate(pointsBuffer, numPointsPerAxis, boundsSize, worldBounds, centre, offset, pointSpacing);

        triangleBuffer.SetCounterValue(0);
        shader.SetBuffer(0, "points", pointsBuffer);
        shader.SetBuffer(0, "triangles", triangleBuffer);
        shader.SetInt("numPointsPerAxis", numPointsPerAxis);
        shader.SetFloat("isoLevel", isoLevel);

        shader.Dispatch(0, numThreadsPerAxis, numThreadsPerAxis, numThreadsPerAxis);

        // Get number of triangles in the triangle buffer
        ComputeBuffer.CopyCount(triangleBuffer, triCountBuffer, 0);
        int[] triCountArray = { 0 };
        triCountBuffer.GetData(triCountArray);
        int numTris = triCountArray[0];

        // Get triangle data from shader
        Triangle[] tris = new Triangle[numTris];
        triangleBuffer.GetData(tris, 0, 0, numTris);

        Mesh mesh = chunk.mesh;

Thanks again @Baste your help is very much appreciated!

It’s not strictly important, but the idea is to only draw a single triangle at a time, and not draw it when you’re not looking at something, as that makes you sure that the code’s doing what you expect. For that i’d set
drawGizmosReady to false if your raycast doesn’t hit anything.

To reduce the size, well, if you don’t understand the code, I’d just play with the variables exposed. I’m guessing that reducing numPointsPerAxis to the minimum value (2) should reduce the complexity a bunch.

Okay @Baste some great news!

Reducing the numPointsPerAxis to 2, as you suggested, has broken my mesh into 2 large triangles, and collision works perfectly like that:

Putting number of tris to 3 appears to work at first…:

but then I click a run it a 2nd, 3rd and 4th time and these are the results:

Another run:

Therefore, the problem appears when it is as simple as 3 points per axis.

Going to try some debugging from here…but thoughts and pointers more than welcome.

Okay so debugging I can’t see why its not working. I also don’t necessarily understand all of what is happening. For example, I don’t know why we do this :

  p0 = vertices[triangles[hit.triangleIndex * 3 + 0]];
            p1 = vertices[triangles[hit.triangleIndex * 3 + 1]];
            p2 = vertices[triangles[hit.triangleIndex * 3 + 2]];

Why do we go into the triangles array and get our index, then multiply it by 3?

I continued to debug, and thought, why don’t I draw all the triangles on my mesh? So I added a bit of code in the MeshGenerator to do that, just to validate that the collision triangle at least matches a triangle on my mesh:

So, I am working on something similar. I think a key point here is how the mesh is treated vs how the raycast triangleIndex is generated. Raycast hit is acting on the MeshCollider, but that collider uses the sharedMesh (stored) to generate the triangles. If you are attempting to match this up with a generated mesh that is not stored back out to that sharedMesh, then the triangle indices won’t match up.

In my example, I have a mesh that I iterate through and split normal-positive faces into submeshes for selection. This works and I can index the triangles myself and see what I expect. If I try to match these up with raycast collision, the triangleIndex matches in the sharedMesh, but the triangles of the sharedMesh do not match with the order of the triangles in my submesh version of the mesh instance. Thus, I cannot rely on the index this way.

Left list is sharedMesh triangles, right list is mesh triangles. Top is triangle index, but that index only works on the left list as I expect it to.

UPDATE:

Just change the MeshCollider to solve the above:

        splitMesh ();
        meshCollider.sharedMesh = mesh;