Files
2025-07-19 01:03:02 +02:00

369 lines
15 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Dynamics.PhysBone.Components;
//This work is licensed under the Creative Commons Attribution-NonCommercial 2.0 License.
//To view a copy of the license, visit https://creativecommons.org/licenses/by-nc/2.0/legalcode
//Made by Dreadrith#3238
//Discord: https://discord.gg/ZsPfrGn
//Github: https://github.com/Dreadrith/DreadScripts
//Gumroad: https://gumroad.com/dreadrith
//Ko-fi: https://ko-fi.com/dreadrith
namespace DreadScripts.PhysBoneConverter
{
public class PhysBoneConverter : EditorWindow
{
#region Automated
private static readonly Dictionary<VRCPhysBoneCollider, DynamicBoneColliderBase> colliderDictionary = new Dictionary<VRCPhysBoneCollider, DynamicBoneColliderBase>();
private static readonly AnimationCurve AngleToStiffnessCurve = SmoothCurveTangents(new AnimationCurve(
new Keyframe(0, 1),
new Keyframe(60, 0.5f),
new Keyframe(105, 0.2f),
new Keyframe(130, 0.1f),
new Keyframe(180, 0)));
private static readonly GUIContent[] names =
{
new GUIContent("Damping","How much the bones get slowed down."),
new GUIContent("Elasticity","How much force is applied to return each bone to its original orientation."),
new GUIContent("Stiffness", "How much the bone's original orientation gets preserved."),
new GUIContent("Inert", "How much the character's position change is ignored in physics simulation."),
new GUIContent("Friction", "How much the bones get slowed down when colliding.")
};
private static int optionsCount;
private static FieldInfo frictionField;
private static FieldInfo frictionDistribField;
#endregion
#region Input
public static GameObject targetRoot;
public static bool makeBackup = true;
public static bool[] bools = {true,true,true,true,true};
public static float[] floats = {0.2f, 0.2f, 0, 0, 0};
public static AnimationCurve[] curves = {new AnimationCurve(), new AnimationCurve(), new AnimationCurve(), new AnimationCurve(), new AnimationCurve()};
#endregion
#region GUI
[MenuItem("DreadTools/Utility/PhysBone Converter")]
public static void ShowWindow() => GetWindow<PhysBoneConverter>("PhysBone Converter");
public void OnGUI()
{
EditorGUIUtility.labelWidth = 90;
using (new GUILayout.HorizontalScope(GUI.skin.box))
targetRoot = (GameObject) EditorGUILayout.ObjectField(Helper.Content.root, targetRoot, typeof(GameObject), true);
for (int i = 0; i < optionsCount; i++)
DrawFloatOption(i);
using (new EditorGUI.DisabledScope(!targetRoot))
if (GUILayout.Button("Convert", Helper.Styles.toolbarButton))
Helper.ReportCall(StartConversion);
EditorGUIUtility.labelWidth = 0;
Helper.Credit();
}
private static void DrawFloatOption(int index)
{
using (new GUILayout.HorizontalScope(GUI.skin.box))
{
EditorGUILayout.PrefixLabel(names[index]);
if (!bools[index])
{
floats[index] = EditorGUILayout.Slider(floats[index], 0f, 1f);
curves[index] = EditorGUILayout.CurveField(curves[index], GUILayout.Width(70));
}
else GUILayout.FlexibleSpace();
var ogColor = GUI.backgroundColor;
GUI.backgroundColor = bools[index] ? Color.green : Color.red;
bools[index] = GUILayout.Toggle(bools[index], "Auto", GUI.skin.button, GUILayout.ExpandWidth(false));
GUI.backgroundColor = ogColor;
}
}
private void OnEnable()
{
targetRoot = targetRoot ?? FindObjectOfType<VRCAvatarDescriptor>()?.gameObject ?? FindObjectOfType<Animator>()?.gameObject;
frictionField = typeof(DynamicBone).GetField("m_Friction");
frictionDistribField = typeof(DynamicBone).GetField("m_FrictionDistrib");
optionsCount = frictionField == null ? 4 : 5;
}
#endregion
#region Main
public static void StartConversion()
{
if (!targetRoot) return;
colliderDictionary.Clear();
var pbBones = targetRoot.GetComponentsInChildren<VRCPhysBone>(true);
var pbColliders = targetRoot.GetComponentsInChildren<VRCPhysBoneCollider>(true);
if (pbBones.Length == 0 && pbColliders.Length == 0)
{
Helper.GreenLog("No Physbones or colliders to convert.");
return;
}
if (makeBackup)
{
var t = targetRoot.transform;
var backup = Instantiate(targetRoot, t.position, t.rotation, t.parent);
backup.name = backup.name.Replace(" (Backup)", string.Empty).Replace("(Clone)", " (Backup)");
backup.SetActive(false);
}
foreach (var c in pbColliders) ConvertCollider(c);
//1 to 1 Replacement is rarely incorrect.
//In cases where there are multiple children of the PB and it's set to ignore, then each child needs its own DBone.
foreach (var pb in pbBones) ConvertPhysBone(pb);
foreach (var c in pbColliders) DestroyImmediate(c);
foreach (var pb in pbBones) DestroyImmediate(pb);
Helper.GreenLog($"Finished conversion! Bones: {pbBones.Length} | Colliders: {pbColliders.Length}");
}
private static void ConvertCollider(VRCPhysBoneCollider pbc)
{
Transform rootTransform = pbc.GetRootTransform();
string collidersName = $"{rootTransform.name} Colliders";
GameObject colliderParent = rootTransform.Find(collidersName)?.gameObject;
if (!colliderParent)
{
colliderParent = new GameObject(collidersName)
{
transform =
{
parent = rootTransform,
localPosition = Vector3.zero,
localRotation = Quaternion.identity,
localScale = Vector3.one
}
};
}
GameObject colliderTarget = new GameObject("Collider")
{
transform =
{
parent = colliderParent.transform,
localPosition = pbc.position,
localRotation = pbc.rotation,
localScale = Vector3.one
}
};
GameObjectUtility.EnsureUniqueNameForSibling(colliderTarget);
bool isPlane = pbc.shapeType == VRC.Dynamics.VRCPhysBoneColliderBase.ShapeType.Plane;
DynamicBoneColliderBase baseCollider;
if (isPlane) baseCollider = colliderTarget.AddComponent<DynamicBonePlaneCollider>();
else baseCollider = colliderTarget.AddComponent<DynamicBoneCollider>();
baseCollider.m_Bound = (DynamicBoneColliderBase.Bound) (pbc.insideBounds ? 1 : 0);
baseCollider.m_Center = Vector3.zero;
baseCollider.m_Direction = DynamicBoneColliderBase.Direction.Y;
if (!isPlane)
{
var collider = (DynamicBoneCollider)baseCollider;
collider.m_Radius = pbc.radius;
collider.m_Height = pbc.height;
}
colliderDictionary.Add(pbc, baseCollider);
}
private static void ConvertPhysBone(VRCPhysBone pb)
{
var dbone = pb.gameObject.AddComponent<DynamicBone>();
dbone.m_Root = pb.GetRootTransform();
float scaleFactor = (Mathf.Abs(dbone.transform.lossyScale.x / dbone.m_Root.lossyScale.x));
dbone.m_Damping = bools[0] ? 1 - pb.spring : floats[0];
dbone.m_DampingDistrib = bools[0] ? InvertCurve(pb.springCurve) : curves[0];
dbone.m_Elasticity = bools[1] ? pb.pull : floats[1];
dbone.m_ElasticityDistrib = bools[1] ? pb.pullCurve : curves[1];
dbone.m_Inert = bools[3] ? pb.immobile : floats[3];
dbone.m_InertDistrib = bools[3] ? pb.immobileCurve : curves[3];
if (!bools[4])
{
frictionField.SetValue(dbone, floats[4]);
frictionDistribField.SetValue(dbone, curves[4]);
}
dbone.m_Radius = pb.radius / scaleFactor;
dbone.m_RadiusDistrib = pb.radiusCurve;
dbone.m_Gravity = new Vector3(0, -pb.gravity / 10f, 0);
//Better Limit conversion, such as Hinge to Freeze Axis, can be done, but not all possibilities with PhysBone limits can be achieved.
if (bools[2])
{
dbone.m_Stiffness = 0;
if (pb.limitType == VRC.Dynamics.VRCPhysBoneBase.LimitType.Angle)
{
bool hasAnglecurve = pb.maxAngleXCurve != null && pb.maxAngleXCurve.keys.Length > 0;
dbone.m_Stiffness = hasAnglecurve ? 1 : AngleToStiffnessCurve.Evaluate(pb.maxAngleX);
dbone.m_StiffnessDistrib = SmoothCurveTangents(DeriveCurve(SubdivideCurve(pb.maxAngleXCurve, 10, false), k =>
{
k.value = AngleToStiffnessCurve.Evaluate(k.value * 180);
return k;
}));
}
}
else
{
dbone.m_Stiffness = floats[2];
dbone.m_StiffnessDistrib = curves[2];
}
dbone.m_Exclusions = pb.ignoreTransforms;
dbone.m_Colliders = pb.colliders.Cast<VRCPhysBoneCollider>()
.Where(pbc => pbc && colliderDictionary.ContainsKey(pbc))
.Select(pbc => colliderDictionary[pbc]).ToList();
if (pb.endpointPosition != Vector3.zero)
{
foreach (var t in dbone.m_Root.GetComponentsInChildren<Transform>().Where(t => t.childCount == 0 && !IsExcluded(t, dbone.m_Exclusions)))
{
GameObjectUtility.EnsureUniqueNameForSibling(new GameObject($"{t.name} EndBone")
{
transform =
{
parent = t,
position = t.TransformPoint(pb.endpointPosition),
localRotation = Quaternion.identity,
localScale = Vector3.one,
}
});
}
}
//Dunno if this optimizes anything in-game but aye better than nothing
dbone.m_DistantDisable = true;
dbone.m_ReferenceObject = targetRoot.transform;
}
private static bool IsExcluded(Transform t, IEnumerable<Transform> exclusions) => exclusions.Any(e => e != null && t.IsChildOf(e));
#endregion
#region Curve Functions
private static AnimationCurve SmoothCurveTangents(AnimationCurve curve)
{
if (curve == null) return null;
for (int i = 0; i < curve.keys.Length; i++)
curve.SmoothTangents(i, 0);
return curve;
}
private static AnimationCurve DeriveCurve(AnimationCurve curve, Func<Keyframe, Keyframe> KeyFunc)
{
if (curve == null) return null;
int length = curve.keys.Length;
var newKeys = new Keyframe[length];
for (int i = 0; i < length; i++)
newKeys[i] = KeyFunc(curve.keys[i]);
return new AnimationCurve(newKeys);
}
private static AnimationCurve InvertCurve(AnimationCurve curve) =>
DeriveCurve(curve, k =>
{
k.value = -k.value;
k.inTangent = -k.inTangent;
k.outTangent = -k.outTangent;
return k;
});
private static AnimationCurve SubdivideCurve(AnimationCurve curve, int keyLimit = 10, bool smoothCurve = true)
{
if (curve == null) return null;
if (curve.length <= 1) return curve;
List<Keyframe> keys = new List<Keyframe>(curve.keys);
if (keys.Count > 1)
{
while (keys.Count < keyLimit)
{
var currentKeys = keys.OrderBy(k => k.time).ToList();
for (int i = 0; i < currentKeys.Count - 1 && keys.Count < keyLimit; i++)
{
var time = (currentKeys[i].time + currentKeys[i + 1].time) / 2;
keys.Add(new Keyframe(time, curve.Evaluate(time)));
}
}
}
var newCurve = new AnimationCurve(keys.ToArray());
if (smoothCurve) SmoothCurveTangents(newCurve);
return newCurve;
}
private static void LogCurve(AnimationCurve curve)
{
Debug.Log(string.Join("\n", curve.keys.Select(k => k.time).Zip(curve.keys.Select(k => k.value), (t, v) => $"Time: {t} | Value: {v}")));
}
#endregion
}
public static class Helper
{
internal static void ReportCall(Action action)
{
try { action(); }
catch (Exception e)
{
string errorMSG = $"{e.Message}\n\n{e.StackTrace}";
if (EditorUtility.DisplayDialog("Error!", $"An unexpected error has occured and execution was halted. Please Press Copy and report this stack trace to Dreadrith#3238\n~~~~~~~~~~~~\n{errorMSG}", "Copy", "Nah"))
EditorGUIUtility.systemCopyBuffer = errorMSG;
throw;
}
}
internal static void Credit()
{
using (new GUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("Made By Dreadrith#3238", "boldlabel"))
Application.OpenURL("https://linktr.ee/Dreadrith");
}
}
private const string logName = "PBConverter";
internal static void GreenLog(string msg) =>
Debug.Log($"<color=green>[{logName}]</color> {msg}");
public static class Styles
{
public static readonly GUIStyle toolbarButton = GUI.skin.GetStyle("toolbarbutton");
public static readonly GUIStyle box = GUI.skin.GetStyle("box");
}
public static class Content
{
public static readonly GUIContent root = new GUIContent("Root", "The Root. Will convert all the PhysBones and Colliders under this root.");
}
}
}