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