├── FACS01 Utilities └── Editor │ ├── FACSGUIStyles.cs │ └── PhysBonesToDynBones.cs └── README.md /FACS01 Utilities/Editor/FACSGUIStyles.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace FACS01.Utilities 4 | { 5 | public class FACSGUIStyles 6 | { 7 | public GUIStyle helpbox; 8 | public GUIStyle dropdownbutton; 9 | public GUIStyle button; 10 | public GUIStyle helpboxSmall; 11 | public GUIStyle buttonSmall; 12 | 13 | public FACSGUIStyles() 14 | { 15 | helpbox = new GUIStyle("HelpBox") 16 | { 17 | richText = true, 18 | alignment = TextAnchor.MiddleCenter, 19 | fontSize = 13, 20 | wordWrap = true 21 | }; 22 | 23 | dropdownbutton = new GUIStyle("dropdownbutton") 24 | { 25 | richText = true, 26 | alignment = TextAnchor.MiddleLeft 27 | }; 28 | 29 | button = new GUIStyle("button") 30 | { 31 | richText = true, 32 | alignment = TextAnchor.MiddleCenter, 33 | fontSize = 13 34 | }; 35 | 36 | helpboxSmall = new GUIStyle("HelpBox") 37 | { 38 | richText = true, 39 | alignment = TextAnchor.MiddleCenter, 40 | fontSize = 12, 41 | wordWrap = true, 42 | padding = new RectOffset(4, 4, 1, 2) 43 | }; 44 | 45 | buttonSmall = new GUIStyle("button") 46 | { 47 | richText = true, 48 | alignment = TextAnchor.MiddleCenter, 49 | fontSize = 12, 50 | padding = new RectOffset(4, 4, 1, 2) 51 | }; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /FACS01 Utilities/Editor/PhysBonesToDynBones.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using VRC.Dynamics; 8 | using VRC.SDK3.Dynamics.PhysBone; 9 | using VRC.SDK3.Dynamics.PhysBone.Components; 10 | 11 | namespace FACS01.Utilities 12 | { 13 | public class PhysBonesToDynBones : EditorWindow 14 | { 15 | private static FACSGUIStyles FacsGUIStyles; 16 | private static EditorWindow window; 17 | private static GameObject ToConvert; 18 | private static bool makeDuplicate; 19 | private static string output_print; 20 | 21 | private static AnimationCurve MaxAngleToStiff; 22 | private static VRCPhysBoneCollider[] pbcList; 23 | private static List dbcList; 24 | 25 | [MenuItem("FACS Utils/Misc/PhysBones to Dynamic Bones", false, 1000)] 26 | public static void ShowWindow() 27 | { 28 | SelectionChange(); ToConvert = null; 29 | window = GetWindow(typeof(PhysBonesToDynBones), false, "PhysBones To DynBones", true); 30 | window.maxSize = new Vector2(500, 400); 31 | } 32 | 33 | private static void SelectionChange() 34 | { 35 | makeDuplicate = true; output_print = ""; 36 | } 37 | 38 | public void OnGUI() 39 | { 40 | if (FacsGUIStyles == null) { FacsGUIStyles = new FACSGUIStyles(); FacsGUIStyles.helpbox.alignment = TextAnchor.MiddleCenter; } 41 | 42 | EditorGUILayout.LabelField($"PhysBones to Dynamic Bones\nScans the selected GameObject and converts all VRCPhysbone bones and colliders (sphere, capsule and plane) to Dynamic Bones.", FacsGUIStyles.helpbox); 43 | 44 | EditorGUI.BeginChangeCheck(); 45 | ToConvert = (GameObject)EditorGUILayout.ObjectField(ToConvert, typeof(GameObject), true, GUILayout.Height(40)); 46 | if (EditorGUI.EndChangeCheck()) SelectionChange(); 47 | 48 | if (ToConvert) 49 | { 50 | makeDuplicate = GUILayout.Toggle(makeDuplicate, "Make Duplicate? (don't edit original)", GUILayout.Height(30)); 51 | if (GUILayout.Button("Convert!", FacsGUIStyles.button, GUILayout.Height(40))) Conversion(); 52 | } 53 | 54 | if (!String.IsNullOrEmpty(output_print)) EditorGUILayout.LabelField(output_print, FacsGUIStyles.helpbox); 55 | } 56 | 57 | private void Conversion() 58 | { 59 | output_print = ""; InitConversionTables(); 60 | 61 | pbcList = ToConvert.GetComponentsInChildren(true); 62 | var pbList = ToConvert.GetComponentsInChildren(true); 63 | if (pbcList.Length == 0 && pbList.Length == 0) 64 | { 65 | output_print = "There is no VRCPhysBone or VRCPhysBoneCollider in this GameObject!"; 66 | return; 67 | } 68 | if (makeDuplicate) 69 | { 70 | MakeDuplicate(); 71 | pbcList = ToConvert.GetComponentsInChildren(true); 72 | pbList = ToConvert.GetComponentsInChildren(true); 73 | } 74 | dbcList = new List(); 75 | foreach (var col in pbcList) 76 | { 77 | if (col.shapeType == VRCPhysBoneColliderBase.ShapeType.Plane) AddDBPlaneCol(dbcList, col); 78 | else AddDBCol(dbcList, col); 79 | } 80 | foreach (var bone in pbList) AddDB(bone); 81 | 82 | foreach (var pbc in pbcList) Component.DestroyImmediate(pbc); 83 | foreach (var pb in pbList) Component.DestroyImmediate(pb); 84 | 85 | output_print = $" - VRCPhysBones converted: {pbList.Length}\n" + 86 | $" - VRCPhysBoneCollider converted: {dbcList.Count}"; 87 | } 88 | 89 | private void AddDB(VRCPhysBone physbone) 90 | { 91 | physbone.InitTransforms(false); 92 | 93 | //fixing 94 | if (physbone.rootTransform == null) physbone.rootTransform = physbone.transform; 95 | if (physbone.ignoreTransforms != null) 96 | { 97 | physbone.ignoreTransforms = physbone.ignoreTransforms.Where(t => t != null).ToList(); 98 | if (physbone.ignoreTransforms.Count == 0) physbone.ignoreTransforms = null; 99 | } 100 | if (physbone.colliders != null) 101 | { 102 | physbone.colliders = physbone.colliders.Where(c => c != null).ToList(); 103 | if (physbone.colliders.Count == 0) physbone.colliders = null; 104 | } 105 | 106 | var newBone = physbone.gameObject.AddComponent(); 107 | 108 | //lossless 109 | newBone.enabled = physbone.enabled; 110 | newBone.m_Root = physbone.rootTransform; 111 | newBone.m_Exclusions = physbone.ignoreTransforms; 112 | newBone.m_Elasticity = physbone.pull; 113 | newBone.m_ElasticityDistrib = physbone.pullCurve; 114 | newBone.m_Inert = physbone.immobile; 115 | newBone.m_InertDistrib = physbone.immobileCurve; 116 | newBone.m_Radius = physbone.radius * Mathf.Abs(physbone.rootTransform.lossyScale.x) / Mathf.Abs(physbone.transform.lossyScale.x); 117 | newBone.m_RadiusDistrib = physbone.radiusCurve; 118 | 119 | //lossy 120 | var fa = DynamicBone.FreezeAxis.None; 121 | if (physbone.limitType == VRCPhysBoneBase.LimitType.Hinge) 122 | { 123 | if (physbone.staticFreezeAxis == Vector3.right) fa = DynamicBone.FreezeAxis.X; 124 | else if (physbone.staticFreezeAxis == Vector3.up) fa = DynamicBone.FreezeAxis.Y; 125 | else if (physbone.staticFreezeAxis == Vector3.forward) fa = DynamicBone.FreezeAxis.Z; 126 | } 127 | newBone.m_FreezeAxis = fa; 128 | 129 | float f2 = Mathf.Max(1E-05f, AverageWorldBoneLength(physbone)); 130 | float f = -physbone.gravity * f2 / Mathf.Abs(physbone.transform.lossyScale.x); 131 | if (physbone.gravityFalloff == 1f) 132 | { 133 | newBone.m_Gravity = new Vector3(0f, f, 0f); 134 | } 135 | else if (physbone.gravityFalloff == 0f) 136 | { 137 | newBone.m_Force = new Vector3(0f, f, 0f); 138 | } 139 | else 140 | { 141 | float f3 = Mathf.Round(100000000f * Mathf.Sin(2f * Mathf.PI * physbone.gravityFalloff)) / 100000000f; 142 | float f4 = Mathf.Round(100000000f * Mathf.Cos(2f * Mathf.PI * physbone.gravityFalloff)) / 100000000f; 143 | newBone.m_Gravity = new Vector3(0f, f * f3, 0f); newBone.m_Force = new Vector3(0f, f * f4, 0f); 144 | } 145 | 146 | float damping; AnimationCurve dampingDistrib; 147 | if (physbone.springCurve == null || physbone.springCurve.length == 0 || IsConstantCurve(physbone.springCurve) == (true, 1)) 148 | { 149 | damping = 1 - physbone.spring; dampingDistrib = null; 150 | } 151 | else 152 | { 153 | damping = Math.Min(1, CurveAbsMaxValue(physbone.springCurve, 1, -1)); 154 | Keyframe[] kf = new Keyframe[physbone.springCurve.length]; 155 | for (int i = 0; i < physbone.springCurve.length; i++) 156 | { 157 | float t = physbone.springCurve.keys[i].time; 158 | float v = (1 - physbone.springCurve.keys[i].value)/ damping; 159 | kf[i] = new Keyframe(t, v); 160 | } 161 | dampingDistrib = new AnimationCurve(kf); 162 | for (int i = 0; i < dampingDistrib.length; i++) 163 | { 164 | dampingDistrib.SmoothTangents(i, 0f); 165 | } 166 | } 167 | newBone.m_Damping = damping; 168 | newBone.m_DampingDistrib = dampingDistrib; 169 | 170 | float stiffness; AnimationCurve stiffnessDistrib; 171 | if (physbone.maxAngleXCurve == null || physbone.maxAngleXCurve.length == 0 || IsConstantCurve(physbone.maxAngleXCurve) == (true, 1)) 172 | { 173 | stiffness = MaxAngleToStiff.Evaluate(physbone.maxAngleX); 174 | stiffnessDistrib = null; 175 | } 176 | else 177 | { 178 | var ts = TrueStiffnessCurve(physbone.maxAngleXCurve); 179 | stiffness = Math.Min(1, CurveAbsMaxValue(ts, 0, 1)); 180 | Keyframe[] kfs = new Keyframe[physbone.maxAngleXCurve.length]; 181 | for (int i = 0; i < physbone.maxAngleXCurve.length; i++) 182 | { 183 | float t = physbone.maxAngleXCurve.keys[i].time; 184 | float v = ts.keys[i].value / stiffness; 185 | kfs[i] = new Keyframe(t, v); 186 | } 187 | stiffnessDistrib = new AnimationCurve(kfs); 188 | for (int i = 0; i < stiffnessDistrib.length; i++) 189 | { 190 | stiffnessDistrib.SmoothTangents(i, 0); 191 | } 192 | } 193 | newBone.m_Stiffness = stiffness; 194 | newBone.m_StiffnessDistrib = stiffnessDistrib; 195 | 196 | if (physbone.colliders != null && physbone.colliders.Count > 0) 197 | { 198 | List cols = new List(); 199 | foreach (var col in physbone.colliders) 200 | { 201 | int ind = Array.IndexOf(pbcList, col); 202 | if (ind >= 0 && dbcList[ind] != null) 203 | { 204 | cols.Add(dbcList[ind]); 205 | } 206 | } 207 | newBone.m_Colliders = cols; 208 | } 209 | } 210 | 211 | private AnimationCurve TrueStiffnessCurve(AnimationCurve ac) 212 | { 213 | Keyframe[] kfs = new Keyframe[ac.length]; 214 | for (int i = 0; i < kfs.Length; i++) 215 | { 216 | float num = i / (float)(kfs.Length - 1); 217 | float num2 = MaxAngleToStiff.Evaluate(180f * ac.keys[i].value); 218 | kfs[i] = new Keyframe(num, num2); 219 | } 220 | AnimationCurve nac = new AnimationCurve(kfs); 221 | for (int i = 0; i < nac.length; i++) nac.SmoothTangents(i, 0f); 222 | return nac; 223 | } 224 | 225 | private (bool,float) IsConstantCurve(AnimationCurve ac) 226 | { 227 | var val1 = ac.keys[0].value; 228 | for (int i = 1; i < ac.keys.Length; i++) 229 | { 230 | if (val1 != ac.keys[i].value) return (false,-1); 231 | } 232 | return (true, val1); 233 | } 234 | 235 | private float CurveAbsMaxValue(AnimationCurve ac, float delta = 0, float multiplier = 1) 236 | { 237 | float val = delta + multiplier * ac.keys[0].value; if (val < 0) val *= -1; 238 | for (int i = 1; i < ac.keys.Length; i++) 239 | { 240 | var tmp = delta + multiplier * ac.keys[i].value; 241 | if (val < tmp) val = tmp; 242 | else if (val < -tmp) val = -tmp; 243 | } 244 | return val; 245 | } 246 | 247 | private void AddDBCol(List dbcList, VRCPhysBoneCollider col) 248 | { 249 | GameObject go = col.gameObject; 250 | var r = col.radius; 251 | var h = col.shapeType == VRCPhysBoneColliderBase.ShapeType.Capsule ? col.height : 0; 252 | var bound = col.insideBounds ? DynamicBoneColliderBase.Bound.Inside : DynamicBoneColliderBase.Bound.Outside; 253 | Vector3 pos; var dir = DynamicBoneColliderBase.Direction.Y; 254 | if (col.rotation == Quaternion.AngleAxis(-90f, Vector3.forward) || 255 | col.rotation == Quaternion.AngleAxis(90f, Vector3.forward)) 256 | { 257 | dir = DynamicBoneColliderBase.Direction.X; pos = col.position; 258 | } 259 | else if (col.rotation == Quaternion.identity || 260 | col.rotation == Quaternion.AngleAxis(180f, Vector3.forward)) 261 | { 262 | dir = DynamicBoneColliderBase.Direction.Y; pos = col.position; 263 | } 264 | else if (col.rotation == Quaternion.AngleAxis(90f, Vector3.right) || 265 | col.rotation == Quaternion.AngleAxis(-90f, Vector3.right)) 266 | { 267 | dir = DynamicBoneColliderBase.Direction.Z; pos = col.position; 268 | } 269 | else 270 | { 271 | go = AddGO(col.transform, Vector3.zero, col.rotation, "DynBone_Collider"); 272 | pos = Quaternion.Inverse(col.rotation) * col.position; 273 | } 274 | 275 | var newCol = go.AddComponent(); 276 | newCol.enabled = col.enabled; 277 | newCol.m_Center = pos; 278 | newCol.m_Direction = dir; 279 | newCol.m_Bound = bound; 280 | newCol.m_Radius = r; 281 | newCol.m_Height = h; 282 | dbcList.Add(newCol); 283 | } 284 | 285 | private void AddDBPlaneCol(List dbcList, VRCPhysBoneCollider col) 286 | { 287 | GameObject go = col.gameObject; 288 | var dir = DynamicBoneColliderBase.Direction.Y; var bound = DynamicBoneColliderBase.Bound.Outside; Vector3 pos; 289 | if (col.rotation == Quaternion.AngleAxis(-90f, Vector3.forward)) 290 | { 291 | dir = DynamicBoneColliderBase.Direction.X; pos = col.position; 292 | } 293 | else if (col.rotation == Quaternion.AngleAxis(90f, Vector3.forward)) 294 | { 295 | dir = DynamicBoneColliderBase.Direction.X; pos = col.position; 296 | bound = DynamicBoneColliderBase.Bound.Inside; 297 | } 298 | else if (col.rotation == Quaternion.identity) 299 | { 300 | dir = DynamicBoneColliderBase.Direction.Y; pos = col.position; 301 | } 302 | else if (col.rotation == Quaternion.AngleAxis(180f, Vector3.forward)) 303 | { 304 | dir = DynamicBoneColliderBase.Direction.Y; pos = col.position; 305 | bound = DynamicBoneColliderBase.Bound.Inside; 306 | } 307 | else if (col.rotation == Quaternion.AngleAxis(90f, Vector3.right)) 308 | { 309 | dir = DynamicBoneColliderBase.Direction.Z; pos = col.position; 310 | } 311 | else if (col.rotation == Quaternion.AngleAxis(-90f, Vector3.right)) 312 | { 313 | dir = DynamicBoneColliderBase.Direction.Z; pos = col.position; 314 | bound = DynamicBoneColliderBase.Bound.Inside; 315 | } 316 | else 317 | { 318 | go = AddGO(col.transform, Vector3.zero, col.rotation, "DynBone_PlaneCollider"); 319 | pos = Quaternion.Inverse(col.rotation) * col.position; 320 | } 321 | 322 | var newPlane = go.AddComponent(); 323 | newPlane.enabled = col.enabled; 324 | newPlane.m_Center = pos; 325 | newPlane.m_Direction = dir; 326 | newPlane.m_Bound = bound; 327 | dbcList.Add(newPlane); 328 | } 329 | 330 | private GameObject AddGO(Transform root, Vector3 position, Quaternion rotation, string GOname) 331 | { 332 | GameObject newGO = new GameObject(); 333 | newGO.transform.parent = root; 334 | newGO.transform.localPosition = position; 335 | newGO.transform.localScale = Vector3.one; 336 | newGO.transform.localRotation = rotation; 337 | if (root.childCount == 0) newGO.name = GOname; 338 | else newGO.name = GetUniqueName(newGO, GOname); 339 | return newGO; 340 | } 341 | 342 | private void MakeDuplicate() 343 | { 344 | GameObject tmp = GameObject.Instantiate(ToConvert); 345 | tmp.name = GetUniqueName(ToConvert, ToConvert.name + " (DynBones)"); 346 | if (ToConvert.transform.parent) tmp.transform.parent = ToConvert.transform.parent; 347 | tmp.transform.localPosition = ToConvert.transform.localPosition; 348 | tmp.transform.localScale = ToConvert.transform.localScale; 349 | tmp.transform.localRotation = ToConvert.transform.localRotation; 350 | ToConvert.SetActive(false); tmp.SetActive(true); 351 | ToConvert = tmp; 352 | } 353 | 354 | private string GetUniqueName(GameObject GO, string baseName) 355 | { 356 | List GOnames = new List() { "" }; 357 | var rootT = GO.transform.parent; 358 | if (rootT) 359 | { 360 | foreach (Transform t in rootT) 361 | { 362 | if (!GOnames.Contains(t.name)) GOnames.Add(t.name); 363 | } 364 | } 365 | else 366 | { 367 | foreach (GameObject go in GO.scene.GetRootGameObjects()) 368 | { 369 | if (!GOnames.Contains(go.name)) GOnames.Add(go.name); 370 | } 371 | } 372 | GOnames.Remove(GO.name); 373 | return ObjectNames.GetUniqueName(GOnames.ToArray(), baseName); 374 | } 375 | 376 | private void OnDestroy() 377 | { 378 | FacsGUIStyles = null; 379 | ToConvert = null; 380 | output_print = null; 381 | MaxAngleToStiff = null; 382 | pbcList = null; dbcList = null; 383 | } 384 | 385 | private static float AverageWorldBoneLength(VRCPhysBone physBone) 386 | { 387 | float num = 0f; 388 | if (physBone.bones.Count <= 0) return 0f; 389 | int num2 = 0; 390 | for (int i = 0; i < physBone.bones.Count; i++) 391 | { 392 | VRCPhysBoneBase.Bone bone = physBone.bones[i]; 393 | if (bone.childIndex >= 0) 394 | { 395 | VRCPhysBoneBase.Bone bone2 = physBone.bones[bone.childIndex]; 396 | num += Vector3.Distance(bone.transform.position, bone2.transform.position); 397 | num2++; 398 | } 399 | else if (bone.isEndBone && physBone.endpointPosition != Vector3.zero) 400 | { 401 | num += Vector3.Distance(bone.transform.position, bone.transform.TransformPoint(physBone.endpointPosition)); 402 | num2++; 403 | } 404 | } 405 | if (num2 <= 0) return 0f; 406 | return num / (float)num2; 407 | } 408 | 409 | private static void InitConversionTables() 410 | { 411 | if (!PhysBoneMigration.HasInitDBConversionTables) 412 | { 413 | PhysBoneMigration.HasInitDBConversionTables = true; 414 | PhysBoneMigration.StiffToMaxAngle = new AnimationCurve(new Keyframe[] 415 | { 416 | new Keyframe(0f, 180f), 417 | new Keyframe(0.1f, 129f), 418 | new Keyframe(0.2f, 106f), 419 | new Keyframe(0.3f, 89f), 420 | new Keyframe(0.4f, 74f), 421 | new Keyframe(0.5f, 60f), 422 | new Keyframe(0.6f, 47f), 423 | new Keyframe(0.7f, 35f), 424 | new Keyframe(0.8f, 23f), 425 | new Keyframe(0.9f, 11f), 426 | new Keyframe(1f, 0f) 427 | }); 428 | for (int i = 0; i < PhysBoneMigration.StiffToMaxAngle.length; i++) 429 | { 430 | PhysBoneMigration.StiffToMaxAngle.SmoothTangents(i, 0f); 431 | } 432 | } 433 | 434 | var mats = new Keyframe[1801]; 435 | for (int i = 0; i < mats.Length; i++) 436 | { 437 | float n = i / (mats.Length - 1f); 438 | mats[i] = new Keyframe(PhysBoneMigration.StiffToMaxAngle.Evaluate(n),n); 439 | } 440 | MaxAngleToStiff = new AnimationCurve(mats); 441 | } 442 | } 443 | } 444 | #endif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhysBone to DynamicBone 2 | [![](https://img.shields.io/github/downloads/FACS01-01/PhysBone-to-DynamicBone/total.svg)](https://github.com/FACS01-01/PhysBone-to-DynamicBone/releases) 3 | [![](https://img.shields.io/github/v/release/FACS01-01/PhysBone-to-DynamicBone)](https://github.com/FACS01-01/PhysBone-to-DynamicBone/releases/latest) 4 | 5 | If you converted Dynamic Bones to VRChat PhysBones, this tool will help you revert it! 6 | 7 | 8 | VRChat doesn't use all Dynamic Bone parameters, and in some cases combines 2 parameters into one, so a full 1-to-1 restoration isn't possible. 9 | This is the closest it can get. 10 | 11 | 12 | Lossless restoration of: 13 | 14 | - All colliders (sphere, capsule and plane) 15 | - Elasticity, Elasticity Distribution 16 | - Inert, Inert Distribution 17 | - Radius, Radius Distribution 18 | 19 | 20 | Lossy restoration of: 21 | 22 | - Freeze Axis 23 | - Gravity, Force 24 | - Damping, Damping Distribution 25 | - Stiffness, Stiffness Distribution 26 | 27 | 28 | Extras: 29 | 30 | - For Physbone colliders with custom rotations, an extra GameObject is added to be able to properly rotate the DynamicBone collider 31 | - For Physbones with custom Gravity Falloff: (new Gravity)^2 + (new Force)^2 = (old Gravity)^2 32 | 33 | 34 | Video: 35 | 36 | [![IMAGE ALT TEXT](http://img.youtube.com/vi/ZEjvQAA6ATc/0.jpg)](http://www.youtube.com/watch?v=ZEjvQAA6ATc "VRC PhysBone to Dynamic Bone Conversion Tool") 37 | 38 | Note: 39 | You need to install VRC PhysBone and DynamicBone (v1.2.2 or greater) beforehand to avoid Unity compilation errors. 40 | --------------------------------------------------------------------------------