Auto Rigger

Anonymous
csharp
02/12/2026 3:24 PM
8.5 KB
11
Indexable
using UnityEngine;
using System;

[DisallowMultipleComponent]
public class CapsuleThreeBoneSkinner : MonoBehaviour
{
    [Header("Bone Layout")]
    [Tooltip("Total length (in local Y) covered by the 3 bones. If 0, auto-detect from mesh bounds.")]
    public float chainLength = 0f;

    [Tooltip("Local Y offset for the root bone (relative to mesh center).")]
    public float rootYOffset = 0f;

    [Header("Weighting")]
    [Tooltip("Higher = more 'nearest bone' behavior. Lower = smoother blending across bones.")]
    [Range(0.25f, 8f)]
    public float falloff = 2.0f;

    [Tooltip("Allow blending to 2 closest bones (recommended). If false, each vertex is 100% one bone.")]
    public bool blendTwoBones = true;

    [Header("Output")]
    public Transform rootBone;
    public Transform midBone;
    public Transform tipBone;

    [ContextMenu("Skin Capsule With 3 Bones")]
    public void SkinNow()
    {
        // 1) Get mesh
        var mf = GetComponent<MeshFilter>();
        if (!mf || !mf.sharedMesh)
        {
            Debug.LogError("No MeshFilter/sharedMesh found on this object.");
            return;
        }

        var originalMesh = mf.sharedMesh;
        var mesh = Instantiate(originalMesh); // don't mutate the shared asset

        // 2) Ensure we have a SkinnedMeshRenderer
        var mr = GetComponent<MeshRenderer>();
        var smr = GetComponent<SkinnedMeshRenderer>();
        if (!smr) smr = gameObject.AddComponent<SkinnedMeshRenderer>();

        if (mr) DestroyImmediate(mr);
        DestroyImmediate(mf);

        smr.sharedMesh = mesh;
        smr.updateWhenOffscreen = true;

        // 3) Create bone transforms (as children of this object)
        // Root at lower, mid in middle, tip at upper along local Y.
        var b0 = new GameObject("Bone_Root").transform;
        var b1 = new GameObject("Bone_Mid").transform;
        var b2 = new GameObject("Bone_Tip").transform;

        b0.SetParent(transform, false);
        b1.SetParent(b0, false);
        b2.SetParent(b1, false);

        // 4) Decide chain length from bounds if not provided
        var bounds = mesh.bounds; // in local mesh space
        float len = chainLength;
        if (len <= 0f)
            len = bounds.size.y;

        // Put the chain centered on the mesh bounds, with optional offset.
        float yMin = bounds.center.y - (len * 0.5f) + rootYOffset;
        float yMax = bounds.center.y + (len * 0.5f) + rootYOffset;

        float y0 = yMin;
        float y1 = Mathf.Lerp(yMin, yMax, 0.5f);
        float y2 = yMax;

        // Position bones in local space
        b0.localPosition = new Vector3(0f, y0, 0f);
        b1.localPosition = new Vector3(0f, y1 - y0, 0f); // because b1 is child of b0
        b2.localPosition = new Vector3(0f, y2 - y1, 0f); // because b2 is child of b1

        // Store refs
        rootBone = b0;
        midBone = b1;
        tipBone = b2;

        // 5) Build bindposes
        // bindpose transforms vertices from mesh space into bone space
        Transform[] bones = { b0, b1, b2 };
        var bindPoses = new Matrix4x4[bones.Length];
        for (int i = 0; i < bones.Length; i++)
        {
            bindPoses[i] = bones[i].worldToLocalMatrix * transform.localToWorldMatrix;
        }
        mesh.bindposes = bindPoses;

        // 6) Compute distance-based bone weights per vertex
        var verts = mesh.vertices;
        var boneWeights = new BoneWeight[verts.Length];

        // Bone positions expressed in mesh local space (same as verts)
        Vector3 p0 = b0.localPosition;
        Vector3 p1 = b0.localPosition + b1.localPosition;
        Vector3 p2 = b0.localPosition + b1.localPosition + b2.localPosition;

        for (int v = 0; v < verts.Length; v++)
        {
            Vector3 p = verts[v];

            float d0 = Mathf.Abs(p.y - p0.y);
            float d1 = Mathf.Abs(p.y - p1.y);
            float d2 = Mathf.Abs(p.y - p2.y);

            if (!blendTwoBones)
            {
                int best = (d0 < d1) ? ((d0 < d2) ? 0 : 2) : ((d1 < d2) ? 1 : 2);
                BoneWeight bw = new BoneWeight();
                bw.boneIndex0 = best;
                bw.weight0 = 1f;
                boneWeights[v] = bw;
                continue;
            }

            // Convert distance to weight via inverse-power falloff
            // w = 1 / (d^falloff + eps)
            const float eps = 1e-4f;
            float w0 = 1f / (Mathf.Pow(d0 + eps, falloff));
            float w1 = 1f / (Mathf.Pow(d1 + eps, falloff));
            float w2 = 1f / (Mathf.Pow(d2 + eps, falloff));

            // Pick top 2 weights for a clean two-bone blend
            int iA = 0, iB = 1;
            float a = w0, b = w1;

            // compare w2
            if (w2 > a)
            {
                iB = iA; b = a;
                iA = 2;  a = w2;
            }
            else if (w2 > b)
            {
                iB = 2; b = w2;
            }

            // Now ensure iA is actually the max among w0,w1,w2 (covers the initial choice)
            // (Already mostly done, but handle case where w1 > w0 etc.)
            // We'll just rebuild using array for clarity.
            float[] ws = { w0, w1, w2 };
            iA = 0; iB = 1;
            if (ws[1] > ws[0]) { iA = 1; iB = 0; }
            if (ws[2] > ws[iA]) { iB = iA; iA = 2; }
            else if (ws[2] > ws[iB]) { iB = 2; }

            a = ws[iA];
            b = ws[iB];

            float sum = a + b;
            a /= sum;
            b /= sum;

            BoneWeight bw2 = new BoneWeight();
            bw2.boneIndex0 = iA; bw2.weight0 = a;
            bw2.boneIndex1 = iB; bw2.weight1 = b;
            boneWeights[v] = bw2;
        }

        mesh.boneWeights = boneWeights;

        // 7) Assign renderer bones + root
        smr.bones = bones;
        smr.rootBone = b0;

        // 8) Basic bounds (optional): expand a little so deformations don’t get culled
        var b = mesh.bounds;
        b.Expand(new Vector3(0.1f, 0.1f, 0.1f));
        mesh.bounds = b;

        Debug.Log("Skinned capsule with 3-bone Y chain: Bone_Root -> Bone_Mid -> Bone_Tip");
    }

    private void Reset()
    {
        // Handy defaults for capsules
        falloff = 2f;
        blendTwoBones = true;
    }

    private void OnValidate()
    {
        falloff = Mathf.Max(0.25f, falloff);
    }
}
Editor is loading...
Leave a Comment