0
\$\begingroup\$

I have tried to automatically generate capsule colliders from bones by temporarily creating a mesh collider and using raycast to find the distance to it from the bone, but when I create the capsule collider there seems to be some rounding errors that makes them very big for bones far down in hierarchy. I am doing this to make cloth physics work.

Any suggestions what might be wrong? I have tried to convert to from global and local space to the best of my knowledge, so am wondering if the only solution is to flatten the bone hierarchy.

Code example (needs Editor Coroutines installed):

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Unity.EditorCoroutines.Editor;


public class AutomaticColliderFitting : ScriptableWizard
{
    [field: SerializeField]
    public SkinnedMeshRenderer SkinnedMeshRenderer { get; set; }

    [field: SerializeField]
    public Cloth[] Clothes { get; set; }

    [MenuItem("Tools/Fit Colliders To Bones")]
    static void CreateWizard()
    {
        CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
        CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
        DisplayWizard<AutomaticColliderFitting>("Automatic Collider Fitting", "Fit");
    }

    IEnumerator ProcessBones()
    {
        var queriesHitBackfaces = Physics.queriesHitBackfaces;

        List<CapsuleCollider> colliders = new List<CapsuleCollider>();

        var meshCollider = this.SkinnedMeshRenderer.transform.root.gameObject.AddComponent<MeshCollider>();
        try
        {
            Physics.queriesHitBackfaces = true;

            meshCollider.sharedMesh = this.SkinnedMeshRenderer.sharedMesh;

            var maxDistance = this.SkinnedMeshRenderer.bounds.size.magnitude;

            foreach(var bone in this.SkinnedMeshRenderer.bones)
            {
                if(bone == null) continue;

                List<Vector3> X = new List<Vector3> { Vector3.left, Vector3.right };
                List<Vector3> Y = new List<Vector3> { Vector3.up, Vector3.down };
                List<Vector3> Z = new List<Vector3> { Vector3.forward, Vector3.back };

                var resultX = RayCast(meshCollider, maxDistance, bone, X).ToList();
                var resultY = RayCast(meshCollider, maxDistance, bone, Y).ToList();
                var resultZ = RayCast(meshCollider, maxDistance, bone, Z).ToList();

                var closestToBoneX = resultX.Where(x => x.hit.collider != null).Select(x => (x.hit.distance, x)).DefaultIfEmpty().Min().x;
                var closestToBoneY = resultY.Where(x => x.hit.collider != null).Select(x => (x.hit.distance, x)).DefaultIfEmpty().Min().x;
                var closestToBoneZ = resultZ.Where(x => x.hit.collider != null).Select(x => (x.hit.distance, x)).DefaultIfEmpty().Min().x;

                var tentative = new List<(Vector3 direction, Ray ray, RaycastHit hit)> { closestToBoneX, closestToBoneY, closestToBoneZ }.Where(x => x.direction != default).ToList();
                if(tentative.Count < 2) { continue; }

                var closest = tentative.Select(x => (x.hit.distance, x)).Min().x;
                tentative.Remove(closest);
                var furthest = tentative.Select(x => (x.hit.distance, x)).Max().x;


                var collider = Undo.AddComponent<CapsuleCollider>(bone.gameObject);
                collider.center = Vector3.zero;
                collider.radius = collider.transform.InverseTransformPoint(closest.hit.point).magnitude;
                collider.height = collider.transform.InverseTransformPoint(furthest.hit.point).magnitude;
                if(furthest.direction == Vector3.left || furthest.direction == Vector3.right) { collider.direction = 0; }
                if(furthest.direction == Vector3.up || furthest.direction == Vector3.down) { collider.direction = 1; }
                if(furthest.direction == Vector3.forward || furthest.direction == Vector3.back) { collider.direction = 2; }

                bool overlapped = Physics.ComputePenetration(
                    collider, collider.transform.position, collider.transform.rotation,
                    meshCollider, meshCollider.transform.position, meshCollider.transform.rotation,
                    out Vector3 direction, out float distance
                );

                if(overlapped)
                {
                    Selection.objects = default;
                    Selection.activeObject = collider;
                    SceneView.lastActiveSceneView.LookAtDirect(collider.transform.position, SceneView.lastActiveSceneView.rotation);
                    SceneView.RepaintAll();
                    yield return null;

                    if(EditorUtility.DisplayDialog("CapsuleCollider overlaps MeshCollider.", "Action for CapsuleCollider?", "Add", "Remove"))
                    {
                        colliders.Add(collider);
                    }
                    else
                    {
                        DestroyImmediate(collider);
                    }
                }
                else
                {
                    colliders.Add(collider);
                }
            }
        }
        finally
        {
            DestroyImmediate(meshCollider);
            // Restore setting
            Physics.queriesHitBackfaces = queriesHitBackfaces;
        }

        if(this.Clothes != null)
        {
            foreach(var cloth in this.Clothes) 
            {
                cloth.capsuleColliders = colliders.ToArray();
            }
        }

        static IEnumerable<(Vector3 direction, Ray ray, RaycastHit hit)> RayCast(MeshCollider meshCollider, float maxDistance, Transform bone, List<Vector3> X)
        {
            foreach(var direction in X)
            {
                var ray = new Ray(bone.position, bone.TransformDirection(direction));
                meshCollider.Raycast(ray, out RaycastHit raycastHit, maxDistance);
                yield return (direction, ray, raycastHit);
            }
        }
    }

    /// <summary>
    /// Fit
    /// </summary>
    void OnWizardCreate()
    {
        Undo.RegisterCompleteObjectUndo(this.SkinnedMeshRenderer, "Automatic Collider Fitting");
        Debug.Log("Automatic Collider Fitting.", this.SkinnedMeshRenderer);

        co = EditorCoroutineUtility.StartCoroutineOwnerless(this.ProcessBones());
    }

    private EditorCoroutine co;

    void OnWizardUpdate()
    {
        this.helpString = "Fits colliders to mesh.";
    }
}

Made a super simple character with a Basic Human metarig in Blender:

Example Example

As you can see the capsule gets a really weird size and it gets super big.

I get equally bad results when measuring distance to closest vertex. Will need to try to measure distance to each triangle as well.

\$\endgroup\$
6
  • \$\begingroup\$ We'll have a higher success rate in locating the source of your problem if we can see your code and a reproducible test case that produces unwanted output, so we can test that case to check whether our suggestions help with that particular problem. \$\endgroup\$
    – DMGregory
    Sep 16, 2021 at 0:31
  • \$\begingroup\$ Okay I'll try to dig up the code later. (Haven't touched it in ages) \$\endgroup\$
    – Emil
    Sep 16, 2021 at 1:19
  • \$\begingroup\$ I need to check if bone.position is in local space when it should be in global space... Physics.Raycast seem to use global space while the docs leave the space undefined in Collider.Raycast. Would be typical if such an stupid mistake stopped me for over a year... \$\endgroup\$
    – Emil
    Jan 21, 2022 at 5:45
  • \$\begingroup\$ Whatever is the fix 8 hours of testing alternatives was not enough. Something is very strange with capsule colliders. I wonder if I have to compute the distance with a full metric tensor instead of using .magnitude. \$\endgroup\$
    – Emil
    Jan 22, 2022 at 2:31
  • \$\begingroup\$ I found out that using Debug.DrawLine gave some insights. It seems that RayCast can miss the mesh collider. It seems that closestPoint can choose a point inside the meshCollider. I have gotten some improvements using vertices after I found out that I need to use SkinnedMeshRenderer.transform.TransformPoint to map it to global space. No solution yet but maybe a bit closer ! \$\endgroup\$
    – Emil
    Jan 23, 2022 at 21:10

1 Answer 1

0
\$\begingroup\$

I will just post the code that seems to work good enough for me at the moment. There were several things that went wrong in my initial answer (for example the MeshCollider was not aligned with the SkinnedMesh). There are probably ways to improve this but for others interested this seems to kind-of work:

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Unity.EditorCoroutines.Editor;
using System;
using System.Reflection;

public static class ColliderExtensions
{
    internal static int GetDirection(this CapsuleCollider collider, Vector3 worldVertex)
    {
        float x = Math.Min(Vector3.Angle(collider.transform.right, worldVertex - collider.transform.position), Vector3.Angle(-collider.transform.right, worldVertex - collider.transform.position));
        float y = Math.Min(Vector3.Angle(collider.transform.up, worldVertex - collider.transform.position), Vector3.Angle(-collider.transform.up, worldVertex - collider.transform.position));
        float z = Math.Min(Vector3.Angle(collider.transform.forward, worldVertex - collider.transform.position), Vector3.Angle(-collider.transform.forward, worldVertex - collider.transform.position));
        if (x <= Math.Min(y, z))
        {
            return 0;
        }
        else if (y <= Math.Min(x, z))
        {
            return 1;
        }
        else if (z <= Math.Min(x, y))
        {
            return 2;
        }
        else
        {
            return 0;
        }
    }

    internal static void UpdateRadius(this CapsuleCollider collider, Vector3 worldVertex)
    {
        Debug.Log($"Using radius {collider.transform.position} -> {worldVertex} with distance {(worldVertex - collider.transform.position).magnitude}.", collider.gameObject);
        Debug.DrawRay(collider.transform.position, worldVertex - collider.transform.position, Color.magenta, 60);
        collider.radius = collider.transform.InverseTransformPoint(worldVertex).magnitude;
    }
    internal static void UpdateHeight(this CapsuleCollider collider, Vector3 worldVertex)
    {
        Debug.Log($"Using height {collider.transform.position} -> {worldVertex} with distance {(worldVertex - collider.transform.position).magnitude}.", collider.gameObject);
        Debug.DrawRay(collider.transform.position, worldVertex - collider.transform.position, Color.cyan, 60);
        collider.height = collider.transform.InverseTransformPoint(worldVertex).magnitude;
    }
    internal static void ReflectionRestoreToBindPose(this GameObject gameObject)
    {
        if (gameObject == null)
            return;
        Type type = Type.GetType("UnityEditor.AvatarSetupTool, UnityEditor");
        if (type != null)
        {
            MethodInfo info = type.GetMethod("SampleBindPose", BindingFlags.Static | BindingFlags.Public);
            if (info != null)
            {
                info.Invoke(null, new object[] { gameObject });
            }
        }
    }
}

public class AutomaticColliderFitting : ScriptableWizard
{
    [field: SerializeField]
    public SkinnedMeshRenderer SkinnedMeshRenderer { get; set; }

    [field: SerializeField]
    public Cloth[] Clothes { get; set; }

    [MenuItem("Tools/Fit Colliders To Bones")]
    static void CreateWizard()
    {
        CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
        CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
        DisplayWizard<AutomaticColliderFitting>("Automatic Collider Fitting", "Fit");
    }

    IEnumerator ProcessBones()
    {
        var queriesHitBackfaces = Physics.queriesHitBackfaces;

        List<CapsuleCollider> colliders = new List<CapsuleCollider>();

        this.SkinnedMeshRenderer.gameObject.ReflectionRestoreToBindPose();

        var meshCollider = this.SkinnedMeshRenderer.transform.root.gameObject.AddComponent<MeshCollider>();
        try
        {
            Physics.queriesHitBackfaces = true;

            Mesh colliderMesh = new Mesh();
            this.SkinnedMeshRenderer.BakeMesh(colliderMesh);
            meshCollider.sharedMesh = null;
            meshCollider.sharedMesh = colliderMesh;

            foreach (var bone in this.SkinnedMeshRenderer.bones)
            {
                if (bone == null) continue;
                var collider = Undo.AddComponent<CapsuleCollider>(bone.gameObject);

                if (UpdateCollider(meshCollider, collider))
                {
                    bool overlapped = Physics.ComputePenetration(
                        collider, collider.transform.position, collider.transform.rotation,
                        meshCollider, meshCollider.transform.position, meshCollider.transform.rotation,
                        out Vector3 _, out float _
                    );

                    if (overlapped)
                    {
                        Debug.Log($"Overlapping {collider.radius} {collider.height}.", collider.gameObject);
                        Selection.objects = default;
                        Selection.activeObject = collider;
                        SceneView.lastActiveSceneView.LookAtDirect(collider.transform.position, SceneView.lastActiveSceneView.rotation);
                        SceneView.RepaintAll();
                        yield return null;

                        if (EditorUtility.DisplayDialog("CapsuleCollider overlaps MeshCollider.", "Action for CapsuleCollider?", "Add", "Remove"))
                        {
                            colliders.Add(collider);
                            continue;
                        }
                        else
                        {
                            DestroyImmediate(collider);
                            continue;
                        }
                    }
                }
                else
                {
                    DestroyImmediate(collider);
                    continue;
                }
            }
        }
        finally
        {
            DestroyImmediate(meshCollider);
            // Restore setting
            Physics.queriesHitBackfaces = queriesHitBackfaces;
        }

        if (this.Clothes != null)
        {
            foreach (var cloth in this.Clothes)
            {
                cloth.capsuleColliders = colliders.ToArray();
            }
        }
    }

    private static bool UpdateCollider(MeshCollider meshCollider, CapsuleCollider collider)
    {
        List<RaycastHit> raycastHits = GetRayCastHits(meshCollider, collider);
        if (raycastHits.Any())
        {
            var firstDirectionClosest = raycastHits.OrderBy(x => x.distance).FirstOrDefault();
            collider.UpdateRadius(firstDirectionClosest.point);
            int firstIgnoreDirection = collider.GetDirection(firstDirectionClosest.point);
            raycastHits.RemoveAll(x => collider.GetDirection(x.point) == firstIgnoreDirection);
            if (raycastHits.Any())
            {
                var secondDirectionClosest = raycastHits.OrderBy(x => x.distance).FirstOrDefault();
                int secondIgnoreDirection = collider.GetDirection(secondDirectionClosest.point);
                raycastHits.RemoveAll(x => collider.GetDirection(x.point) == secondIgnoreDirection);
                if (raycastHits.Any())
                {
                    var thirdDirectionClosest = raycastHits.OrderBy(x => x.distance).FirstOrDefault();
                    collider.UpdateHeight(thirdDirectionClosest.point);
                    collider.direction = collider.GetDirection(thirdDirectionClosest.point);
                    return true;
                }
            }
        }
        return false;
    }

    private static List<RaycastHit> GetRayCastHits(MeshCollider meshCollider, CapsuleCollider collider)
    {
        List<RaycastHit> raycastHits = new List<RaycastHit>();
        foreach (var direction in new[] { collider.transform.up, collider.transform.right, collider.transform.forward, -collider.transform.up, -collider.transform.right, -collider.transform.forward })
        {
            if (meshCollider.Raycast(new Ray(collider.transform.position, collider.transform.TransformDirection(direction)), out RaycastHit hit, meshCollider.bounds.size.magnitude))
            {
                raycastHits.Add(hit);
            }
        }

        return raycastHits;
    }


    /// <summary>
    /// Fit
    /// </summary>
    void OnWizardCreate()
    {
        Undo.RegisterCompleteObjectUndo(this.SkinnedMeshRenderer, "Automatic Collider Fitting");
        Debug.Log("Automatic Collider Fitting.", this.SkinnedMeshRenderer);

        co = EditorCoroutineUtility.StartCoroutineOwnerless(this.ProcessBones());
    }

    private EditorCoroutine co;

    void OnWizardUpdate()
    {
        this.helpString = "Fits colliders to mesh.";
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Remember to click the checkmark in the top left of the answer to mark it as "Accepted" if it solved your problem. \$\endgroup\$
    – DMGregory
    Aug 13, 2023 at 1:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .