Generate 2D polygon collider from 3D mesh

My game use 2d physic, but 3D graphics. The unity editor automatically generates a collider polygon using the sprite of the desired shape. If for example on a sprite a circle is drawn on a transparent background, then the generated collider polygon will be round. When the 2D scene mode is fixed, if you click on the 3d mesh model - it is outlined in orange (as can be seen in the screenshot), I want to make the polygon a collider polygon of the same shape as if it was a sprite. Tell me, please, how can I do this except to manually sketch the collider polygon? I need to algorithm working on at least simple examples as in the screenshot 113561-снимок-экрана-2018-03-23-в-181557.png

I was looking for this exact solution myself (All that dragging around polygon points was driving me insane).

Here’s an editor script I wrote that will match any PolygonCollider2D to the MeshFilter on a GameObject.

Just plop it into an Editor folder, highlight the GameObject, and click Update Polygon Colliders up in the Tools menu, or Ctrl/Cmd+T

Seems to work well with ProBuilder and in Prefab Edit Mode. I’m sure this can be greatly improved upon, feel free!

GIANT credit goes to @Bunny83 for their EdgeHelper class Wouldn’t work nearly as well without it. :slight_smile:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using UnityEditor.SceneManagement;
using System.Linq;

public static class SetPolygonCollider3D
{
    [MenuItem("Tools/Update Polygon Colliders %t", false, -1)]
    static void UpdatePolygonColliders()
    {
        Transform transform = Selection.activeTransform;
        if (transform == null)
        {
            Debug.LogWarning("No valid GameObject selected!");
            return;
        }

        EditorSceneManager.MarkSceneDirty(transform.gameObject.scene);

        MeshFilter[] meshFilters = transform.GetComponentsInChildren<MeshFilter>();

        foreach (MeshFilter meshFilter in meshFilters)
        {
            if (meshFilter.GetComponent<PolygonCollider2D>() != null)
            {
                UpdatePolygonCollider2D(meshFilter);
            }
        }
    }

    static void UpdatePolygonCollider2D(MeshFilter meshFilter)
    {
        if (meshFilter.sharedMesh == null)
        {
            Debug.LogWarning(meshFilter.gameObject.name + " has no Mesh set on its MeshFilter component!");
            return;
        }

        PolygonCollider2D polygonCollider2D = meshFilter.GetComponent<PolygonCollider2D>();
        polygonCollider2D.pathCount = 1;

        List<Vector3> vertices = new List<Vector3>();
        meshFilter.sharedMesh.GetVertices(vertices);

        var boundaryPath = EdgeHelpers.GetEdges(meshFilter.sharedMesh.triangles).FindBoundary().SortEdges();

        Vector3[] yourVectors = new Vector3[boundaryPath.Count];
        for (int i = 0; i < boundaryPath.Count; i++)
        {
            yourVectors _ = vertices[boundaryPath *.v1];
        }
        List<Vector2> newColliderVertices = new List<Vector2>();

        for (int i = 0; i < yourVectors.Length; i++)
        {
            newColliderVertices.Add(new Vector2(yourVectors_.x, yourVectors *.y));
        }

        Vector2[] newPoints = newColliderVertices.Distinct().ToArray();

        EditorUtility.SetDirty(polygonCollider2D);

        polygonCollider2D.SetPath(0, newPoints);
        Debug.Log(meshFilter.gameObject.name + " PolygonCollider2D updated.");
    }
}

public static class EdgeHelpers
{
    public struct Edge
    {
        public int v1;
        public int v2;
        public int triangleIndex;
        public Edge(int aV1, int aV2, int aIndex)
        {
            v1 = aV1;
            v2 = aV2;
            triangleIndex = aIndex;
        }
    }

    public static List<Edge> GetEdges(int[] aIndices)
    {
        List<Edge> result = new List<Edge>();
        for (int i = 0; i < aIndices.Length; i += 3)
        {
            int v1 = aIndices *;
            int v2 = aIndices[i + 1];
            int v3 = aIndices[i + 2];
            result.Add(new Edge(v1, v2, i));
            result.Add(new Edge(v2, v3, i));
            result.Add(new Edge(v3, v1, i));
        }
        return result;
    }

    public static List<Edge> FindBoundary(this List<Edge> aEdges)
    {
        List<Edge> result = new List<Edge>(aEdges);
        for (int i = result.Count - 1; i > 0; i--)
        {
            for (int n = i - 1; n >= 0; n--)
            {
                if (result_.v1 == result[n].v2 && result *.v2 == result[n].v1)
                {
                    // shared edge so remove both
                    result.RemoveAt(i);
                    result.RemoveAt(n);
                    i--;
                    break;
                }
            }
        }
        return result;
    }
    public static List<Edge> SortEdges(this List<Edge> aEdges)
    {
        List<Edge> result = new List<Edge>(aEdges);
        for (int i = 0; i < result.Count - 2; i++)
        {
            Edge E = result *;
            for (int n = i + 1; n < result.Count; n++)
            {
                Edge a = result[n];
                if (E.v2 == a.v1)
                {
                    // in this case they are already in order so just continoue with the next one
                    if (n == i + 1)
                        break;
                    // if we found a match, swap them with the next one after "i"
                    result[n] = result[i + 1];
                    result[i + 1] = a;
                    break;
                }
            }
        }
        return result;
    }
}

In my case, having a large number of points on the collider severely impacts performance (e.g., with over 500 points per rig, the FPS drops to 3), so I need to reduce the number of points on the collider.

The main idea is to generate a PolygonCollider2D by sampling points around a circle and checking for collisions with a mesh using RayCast. Adjusting the _deltaAngle value allows you to control the level of detail in the generated points.

To utilize this functionality, first select the object containing the PolygonCollider2D, then select the GameObject with the MeshCollider. Finally, click on ‘Generate Polygon Colliders’ in the Tools menu or use the shortcut Ctrl/Cmd+T.

Sorry for my bad English

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

public static class ColliderHelper
{
    private static float _deltaAngle = 5f;
    
    
    [MenuItem("Tools/Generate Polygon Colliders %t", false, -1)]
    private static void GenerateCollider()
    {
        var objList = Selection.objects.OfType<GameObject>().ToArray();
        
        PolygonCollider2D polygonCollider2D = objList[0].GetComponent<PolygonCollider2D>();
        
        MeshFilter meshFilter = objList[1].GetComponent<MeshFilter>();
        
        if(meshFilter == null) return;
        
        MeshCollider meshCollider = meshFilter.gameObject.GetComponent<MeshCollider>();

        // ScanCircle(meshFilter, polygonCollider2D, meshCollider);
        AsyncScanCircle(meshFilter, polygonCollider2D, meshCollider);
    }
    
    // Delay for next frame
    private static async void AsyncScanCircle(MeshFilter meshFilter, PolygonCollider2D polygonCollider2D, MeshCollider meshCollider)
    {
        Vector3 previousPosition = meshFilter.transform.position;
        meshFilter.transform.position = Vector3.zero;

        await Task.Delay(50);
        ScanCircle(meshFilter, polygonCollider2D, meshCollider);
        await Task.Delay(10);
        
        meshFilter.transform.position = previousPosition;
    }
    
    
    private static void ScanCircle(MeshFilter meshFilter, PolygonCollider2D polygonCollider2D, MeshCollider meshCollider)
    {
        var points = new List<Vector2>();
        var vertices = meshFilter.sharedMesh.vertices;
        for (var i = 0; i < vertices.Length; i++) 
        {
            points.Add(new Vector2(vertices[i].x, vertices[i].y));
        }

        Vector3 centroid = FindCentroid(points);
        float radius = GetMaxDistance(points, centroid) + 0.01f;
            
            
        List<Vector2> collPoints = new List<Vector2>();
        float angle = 0;
        do
        {
            var pointInCircle = GetPointOnCircle(radius, centroid, angle);
            Vector3 hitPoint;
            if (CheckCollision(meshFilter, pointInCircle, centroid - pointInCircle, out hitPoint))
            {
                collPoints.Add(hitPoint);
            }
            angle += _deltaAngle;
        } while (angle < 360);
            
            
        // collPoints = Sort(Filter(collPoints));
        polygonCollider2D.points = collPoints.ToArray();
    }
    
    
    private static bool CheckCollision(MeshFilter meshFilter, Vector3 origin, Vector3 direction, out Vector3 hitPoint)
    {
        hitPoint = Vector3.zero;
        
        if (meshFilter == null)
        {
            Debug.LogError("MeshFilter not assigned.");
            return false;
        }

        Mesh mesh = meshFilter.sharedMesh;
        if (mesh == null)
        {
            Debug.LogError("Mesh not found.");
            return false;
        }

        Ray ray = new Ray(origin, direction);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit))
        {
            Vector3[] vertices = mesh.vertices;
            int[] triangles = mesh.triangles;

            for (int i = 0; i < triangles.Length; i += 3)
            {
                Vector3 v0 = vertices[triangles[i]];
                Vector3 v1 = vertices[triangles[i + 1]];
                Vector3 v2 = vertices[triangles[i + 2]];

                if (RayTriangleIntersection(origin, direction, v0, v1, v2))
                {
                    hitPoint = hit.point;
                    return true;
                }
            }
        }
        
        return false;
    }

    // Ray-triangle intersection test
    private static bool RayTriangleIntersection(Vector3 orig, Vector3 dir, Vector3 v0, Vector3 v1, Vector3 v2)
    {
        Vector3 edge1 = v1 - v0;
        Vector3 edge2 = v2 - v0;
        Vector3 pVec = Vector3.Cross(dir, edge2);
        float det = Vector3.Dot(edge1, pVec);

        if (det < Mathf.Epsilon) return false;

        Vector3 tVec = orig - v0;
        float u = Vector3.Dot(tVec, pVec);
        if (u < 0 || u > det) return false;

        Vector3 qVec = Vector3.Cross(tVec, edge1);
        float v = Vector3.Dot(dir, qVec);
        if (v < 0 || u + v > det) return false;

        return true;
    }
    private static Vector2 FindCentroid(List<Vector2> points)
    {
        Vector2 centroid = Vector2.zero;
                
        foreach (var point in points)
        {
            centroid += point;
        }
        centroid /= points.Count;
        return centroid;
    }

    private static float GetMaxDistance(List<Vector2> points, Vector2 centroid)
    {
        float maxDistance = 0;
        foreach (var point in points)
        {
            maxDistance = Mathf.Max(maxDistance, Vector2.Distance(point, centroid));
        }

        return maxDistance;
    }
    
    private static Vector3 GetPointOnCircle(float radius, Vector2 center, float angleDegrees)
    {
                
        // Convert angle to radians
        float angleRadians = angleDegrees * Mathf.Deg2Rad;

        // Calculate x and y coordinates
        float x = center.x + radius * Mathf.Cos(angleRadians);
        float y = center.y + radius * Mathf.Sin(angleRadians);

        var point = new Vector3(x, y, 0);
        return point;
    }
}