├── Editor.meta ├── Editor ├── DynamicBonesPresets.txt ├── DynamicBonesPresets.txt.meta ├── DynamicBonesStudioWindow.cs ├── DynamicBonesStudioWindow.cs.meta ├── IniFile.cs ├── IniFile.cs.meta ├── SavePresetWindow.cs └── SavePresetWindow.cs.meta ├── README.md └── README.md.meta /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e26eb27098746dc46ab9f6f050b244eb 3 | folderAsset: yes 4 | timeCreated: 1519715632 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/DynamicBonesPresets.txt: -------------------------------------------------------------------------------- 1 | [AccessoryWhitelist] 2 | skirt=skirt 3 | gimmick_l=gimmick_l 4 | gimmick_r=gimmick_r 5 | ear=ear 6 | ear_l=ear_l 7 | ear_r=ear_r 8 | earr=earr 9 | earl=earl 10 | scarf=scarf 11 | tie=tie 12 | tail=tail 13 | breast_l=breast_l 14 | breast_r=breast_r 15 | bell=bell 16 | gimmickl=gimmickl 17 | gimmickr=gimmickr 18 | gimmick=gimmick 19 | [Default Dynamic Bone] 20 | Name=Default Dynamic Bone 21 | UpdateRate=60 22 | Damp=0.1 23 | Elasticity=0.1 24 | Stiff=0.1 25 | Inert=0 26 | Radius=0 27 | EndOffsetX=0 28 | EndOffsetY=0 29 | EndOffsetZ=0 30 | GravityX=0 31 | GravityY=0 32 | GravityZ=0 33 | ForceX=0 34 | ForceY=0 35 | ForceZ=0 36 | -------------------------------------------------------------------------------- /Editor/DynamicBonesPresets.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a965069efdb796e46a583f411dae77c2 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/DynamicBonesStudioWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using UnityEngine.Assertions.Must; 10 | using UnityEngine.EventSystems; 11 | using UnityEngine.UI; 12 | using VRCSDK2; 13 | 14 | /** 15 | * 16 | * If you're reading this I'm sorry for the spaghet code, I am but a self taught programmer through the university of stack overflow. 17 | * 18 | * Planned features: 19 | * - Colliders? 20 | * - Automatically place colliders on leg/hip/head/index fingers with sliders to easily adjust sizes. 21 | * - Add the colliders to desired bones (hair/ears/skirt/etc) 22 | * 23 | * - Auto root common accessories with empty game objects if available 24 | */ 25 | 26 | public class DynamicBonesPreset 27 | { 28 | public string Name { get; set; } 29 | public Transform Root { get; set; } 30 | public float UpdateRate { get; set; } 31 | public float Damping { get; set; } 32 | public float Elasticity { get; set; } 33 | public float Stiffness { get; set; } 34 | public float Inert { get; set; } 35 | public float Radius { get; set; } 36 | public Vector3 EndLength { get; set; } 37 | public Vector3 EndOffset { get; set; } 38 | public Vector3 Gravity { get; set; } 39 | public Vector3 Force { get; set; } 40 | public Transform[] Colliders { get; set; } 41 | public Transform[] Exclusions { get; set; } 42 | } 43 | 44 | public class DynamicBonesStudioWindow : EditorWindow 45 | { 46 | // Interface values 47 | private int _selectedTabIndex; 48 | private bool _isAutoRefreshEnabled = true; 49 | private bool _isDataSaved; 50 | private bool _isOptionsShowing; 51 | private bool _isAskForBoneNameEnabled; 52 | private bool _isAboutShowing = true; 53 | private bool _isPresetNameSet; 54 | private bool _isEditorLastStatePlaying; 55 | private string _itemToAddToWhitelist = "Enter item name here"; 56 | private Vector2 _studioScrollPos; 57 | 58 | private IniFile _configFile; 59 | 60 | private GameObject _avatar; 61 | private Animator _avatarAnim; 62 | 63 | private List _allDynamicBones = new List(); 64 | private List _dynamicBonePresets = new List(); 65 | //private List _accessoriesBones = new List(); 66 | 67 | // Default values if ini file fails to load. 68 | private List _commonAccessories = 69 | new List { "skirt", "gimmick_l", "gimmick_r", "earl", "earr", "ear_l", "ear_r", "scarf", "tie", "tail", "breast_l", "breast_r", "bell" }; 70 | 71 | private Transform _hairBone; 72 | 73 | // TODO 74 | //private SerializedProperty exclusions = null; 75 | //private SerializedProperty colliders = null; 76 | 77 | [MenuItem("Window/Dynamic Bones Studio")] 78 | public static void ShowWindow() 79 | { 80 | EditorWindow.GetWindow(typeof(DynamicBonesStudioWindow), false, "Dynamic Bones Studio"); 81 | } 82 | 83 | public List ExclusionTransforms = new List(); 84 | public List BonesTransforms = new List(); 85 | public List AccessoriesTransforms = new List(); 86 | 87 | public List AllBones = new List(); 88 | 89 | private SavePresetWindow _presetSaveInstance; 90 | private string _cfgFilePath; 91 | private int _presetChoiceIndex; 92 | 93 | void HandleOnPlayModeChanged() 94 | { 95 | if (_isAutoRefreshEnabled) 96 | { 97 | _allDynamicBones = _avatar.GetComponentsInChildren().ToList(); 98 | } 99 | } 100 | 101 | void OnEnable() 102 | { 103 | // Get the directory name of the filepath of this current script 104 | // This will ensure the config file is always in the same directory as the editor script. 105 | var script = MonoScript.FromScriptableObject(this); 106 | var path = AssetDatabase.GetAssetPath(script); 107 | var dir = Path.GetDirectoryName(path); 108 | 109 | _cfgFilePath = dir + "/DynamicBonesPresets.txt"; 110 | 111 | // Automatically create the file if it isn't found. 112 | if (!File.Exists(_cfgFilePath)) 113 | { 114 | Debug.Log("Config file not found, creating "); 115 | File.Create(dir + "/DynamicBonesPresets.txt"); 116 | } 117 | } 118 | 119 | void OnInspectorUpdate() 120 | { 121 | Repaint(); 122 | } 123 | 124 | void OnGUI() 125 | { 126 | EditorGUILayout.BeginHorizontal(); 127 | _isOptionsShowing = GUILayout.Toggle(_isOptionsShowing, "Show Options", EditorStyles.toolbarButton, new[] { GUILayout.ExpandWidth(false) }); 128 | _selectedTabIndex = GUILayout.Toolbar(_selectedTabIndex, new string[] {"Basic setup", "Studio"}, EditorStyles.toolbarButton); 129 | 130 | EditorApplication.playmodeStateChanged += HandleOnPlayModeChanged; 131 | EditorGUILayout.EndHorizontal(); 132 | 133 | if (_isOptionsShowing) 134 | { 135 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 136 | EditorGUILayout.LabelField("Add item to common accessory whitelist:", EditorStyles.boldLabel); 137 | EditorGUI.indentLevel++; 138 | EditorGUILayout.BeginHorizontal(); 139 | GUI.SetNextControlName("WhitelistTextField"); 140 | 141 | _itemToAddToWhitelist = EditorGUILayout.TextField(_itemToAddToWhitelist); 142 | 143 | if (GUILayout.Button("Add item") /*|| (Event.current.keyCode == KeyCode.Return && GUI.GetNameOfFocusedControl() == "WhitelistTextField")*/) 144 | { 145 | if (_configFile == null) 146 | { 147 | InitConfigFile(); 148 | } 149 | _configFile.SetValue("AccessoryWhitelist", _itemToAddToWhitelist.ToLowerInvariant(), _itemToAddToWhitelist.ToLowerInvariant()); 150 | _configFile.Save(_cfgFilePath); 151 | _configFile.Refresh(); 152 | 153 | } 154 | 155 | EditorGUILayout.EndHorizontal(); 156 | if (GUILayout.Button("Reset All Settings", new []{GUILayout.ExpandWidth(false)})) 157 | { 158 | var result = EditorUtility.DisplayDialog("Confirm", "Are you sure you wish to reset all settings?", 159 | "Yes", "Cancel"); 160 | if (result) 161 | { 162 | _avatar = null; 163 | AccessoriesTransforms.Clear(); 164 | _hairBone = null; 165 | _isAutoRefreshEnabled = false; 166 | _dynamicBonePresets.Clear(); 167 | _allDynamicBones.Clear(); 168 | } 169 | 170 | } 171 | 172 | EditorGUI.indentLevel--; 173 | EditorGUILayout.Space(); 174 | EditorGUILayout.Space(); 175 | EditorGUILayout.EndVertical(); 176 | 177 | } 178 | switch (_selectedTabIndex) 179 | { 180 | // Basic setup tab 181 | case 0: 182 | { 183 | _selectedTabIndex = 0; 184 | 185 | EditorGUILayout.LabelField("Avatar:", EditorStyles.boldLabel); 186 | _avatar = (GameObject) EditorGUILayout.ObjectField(_avatar, typeof(GameObject), true); 187 | 188 | // Check for VRC Avatar Descriptor 189 | if (_avatar != null && _avatar.GetComponent() == null) 190 | { 191 | var result = EditorUtility.DisplayDialog("Error", 192 | "You need to select a game object with a VRC Avatar Descriptor and an Animator!", "Add now", "Cancel"); 193 | if (result) 194 | { 195 | _avatar.AddComponent(); 196 | } 197 | else 198 | { 199 | _avatar = null; 200 | break; 201 | } 202 | } 203 | 204 | // Check if avatar is non-null and has an Animator. 205 | if (_avatar != null && _avatar.GetComponent() == null) 206 | { 207 | EditorUtility.DisplayDialog("Error", 208 | "There is no animator on this Avatar!", "Ok"); 209 | _avatar = null; 210 | break; 211 | } 212 | // If avatar is not assigned exit 213 | if (_avatar == null) 214 | { 215 | //EditorGUILayout.BeginVertical(EditorStyles.helpBox); 216 | EditorGUILayout.HelpBox("To begin, drag a new avatar into the box above!", MessageType.Info); 217 | //EditorGUILayout.EndVertical(); 218 | break; 219 | } 220 | if (!_avatar.GetComponent().avatar.isHuman) 221 | { 222 | EditorUtility.DisplayDialog("Error", 223 | "This model is not humanoid!\nDynamic bones studio only works with humanoid rigs at the moment.", "ok"); 224 | _avatar = null; 225 | return; 226 | } 227 | _avatarAnim = _avatar.GetComponent(); 228 | AllBones = _avatarAnim.GetComponentsInChildren().ToList(); 229 | 230 | EditorGUILayout.LabelField("Hair root Bone:", EditorStyles.boldLabel); 231 | // Try to automatically find hair root. 232 | if (_hairBone == null) 233 | { 234 | _hairBone = TryFindHairRoot(); 235 | } 236 | _hairBone = (Transform) EditorGUILayout.ObjectField(_hairBone, typeof(Transform), true); 237 | 238 | EditorGUILayout.LabelField("Accessories Bones:", EditorStyles.boldLabel); 239 | var accessoriesTarget = this; 240 | var accessoriesSo = new SerializedObject(accessoriesTarget); 241 | var accessoriesTransformsProperty = accessoriesSo.FindProperty("AccessoriesTransforms"); 242 | EditorGUILayout.PropertyField(accessoriesTransformsProperty, true); 243 | accessoriesSo.ApplyModifiedProperties(); 244 | 245 | if (GUILayout.Button("Try and find common accessories")) 246 | { 247 | AccessoriesTransforms = TryFindCommonAccessories(); 248 | if (AccessoriesTransforms != null) 249 | { 250 | Debug.Log("Auto Dynamic Bones - Finding common accessories"); 251 | accessoriesTransformsProperty = accessoriesSo.FindProperty("AccessoriesTransforms"); 252 | EditorGUILayout.PropertyField(accessoriesTransformsProperty, true); 253 | accessoriesSo.ApplyModifiedProperties(); 254 | } 255 | else 256 | { 257 | Debug.Log("Something went wrong finding accessories"); 258 | } 259 | } 260 | if (GUILayout.Button("Apply Dynamic Bones")) 261 | { 262 | if (_hairBone == null) 263 | { 264 | var result = EditorUtility.DisplayDialog("Warning", "No hair transform set, continue?", "Yes", "No"); 265 | if (!result) 266 | { 267 | break; 268 | } 269 | } 270 | AddDynamicBones(); 271 | _allDynamicBones = _avatar.GetComponentsInChildren().ToList(); 272 | } 273 | break; 274 | } 275 | 276 | // Studio tab 277 | case 1: 278 | { 279 | _selectedTabIndex = 1; 280 | 281 | // If avatar is not assigned exit 282 | if (_avatar == null) 283 | { 284 | EditorGUILayout.LabelField("Select an avatar in the basic setup tab to begin", 285 | EditorStyles.boldLabel); 286 | break; 287 | } 288 | 289 | /* Experimental Features */ 290 | GUILayout.Label("Experimental features:", EditorStyles.boldLabel); 291 | 292 | GUILayout.BeginHorizontal(); 293 | _isAutoRefreshEnabled = 294 | EditorGUILayout.ToggleLeft("Auto-refresh bones on state change\n(Enables saving bone settings from play mode)", _isAutoRefreshEnabled, new []{GUILayout.Height(30)}); 295 | 296 | GUILayout.EndHorizontal(); 297 | 298 | _isAskForBoneNameEnabled = 299 | EditorGUILayout.ToggleLeft("Ask for name when saving dynamic bone presets", _isAskForBoneNameEnabled); 300 | 301 | EditorGUILayout.BeginHorizontal(); 302 | 303 | if (GUILayout.Button("Refresh bones")) 304 | { 305 | _allDynamicBones = _avatar.GetComponentsInChildren().ToList(); 306 | Debug.Log("Found " + _allDynamicBones.Count + " new dynamic bones."); 307 | } 308 | 309 | if (GUILayout.Button("Refresh presets")) 310 | { 311 | LoadDynamicBonePresets(); 312 | } 313 | EditorGUILayout.EndHorizontal(); 314 | 315 | /* End Experimental Features */ 316 | 317 | if (EditorApplication.isPlaying) 318 | { 319 | if (_isAutoRefreshEnabled && GUILayout.Button("Save play-mode bone settings")) 320 | { 321 | EditorGUILayout.LabelField("^Will not save colliders or exclusions, beware", EditorStyles.boldLabel); 322 | foreach (var dynamicBone in _allDynamicBones) 323 | { 324 | EditorPrefs.SetFloat(dynamicBone.name + "UpdateRate", dynamicBone.m_UpdateRate); 325 | EditorPrefs.SetFloat(dynamicBone.name + "Damping", dynamicBone.m_Damping); 326 | EditorPrefs.SetFloat(dynamicBone.name + "Elasticity", dynamicBone.m_Elasticity); 327 | EditorPrefs.SetFloat(dynamicBone.name + "Stiffness", dynamicBone.m_Stiffness); 328 | EditorPrefs.SetFloat(dynamicBone.name + "Inert", dynamicBone.m_Inert); 329 | EditorPrefs.SetFloat(dynamicBone.name + "Radius", dynamicBone.m_Radius); 330 | 331 | // End offset vector 332 | EditorPrefs.SetFloat(dynamicBone.name + "EndOffsetX", dynamicBone.m_EndOffset.x); 333 | EditorPrefs.SetFloat(dynamicBone.name + "EndOffsetY", dynamicBone.m_EndOffset.y); 334 | EditorPrefs.SetFloat(dynamicBone.name + "EndOffsetZ", dynamicBone.m_EndOffset.z); 335 | // End offset vector 336 | 337 | // Gravity vector 338 | EditorPrefs.SetFloat(dynamicBone.name + "GravityX", dynamicBone.m_Gravity.x); 339 | EditorPrefs.SetFloat(dynamicBone.name + "GravityY", dynamicBone.m_Gravity.y); 340 | EditorPrefs.SetFloat(dynamicBone.name + "GravityZ", dynamicBone.m_Gravity.z); 341 | // Gravity vector 342 | 343 | // Force vector 344 | EditorPrefs.SetFloat(dynamicBone.name + "ForceX", dynamicBone.m_Force.x); 345 | EditorPrefs.SetFloat(dynamicBone.name + "ForceY", dynamicBone.m_Force.y); 346 | EditorPrefs.SetFloat(dynamicBone.name + "ForceZ", dynamicBone.m_Force.z); 347 | // Force vector 348 | 349 | 350 | } 351 | EditorPrefs.SetBool("DynamicBoneStudioDataSaved", true); 352 | Debug.Log("Prefs saved"); 353 | } 354 | } 355 | 356 | if (EditorPrefs.HasKey("DynamicBoneStudioDataSaved")) 357 | { 358 | if (_isAutoRefreshEnabled && GUILayout.Button("Load play-mode bone settings")) 359 | { 360 | foreach (var dynamicBone in _allDynamicBones) 361 | { 362 | UpdateDynamicBone(dynamicBone, true, 363 | EditorPrefs.GetFloat(dynamicBone.name + "UpdateRate"), 364 | EditorPrefs.GetFloat(dynamicBone.name + "Damping"), 365 | EditorPrefs.GetFloat(dynamicBone.name + "Elasticity"), 366 | EditorPrefs.GetFloat(dynamicBone.name + "Stiffness"), 367 | EditorPrefs.GetFloat(dynamicBone.name + "Inert"), 368 | EditorPrefs.GetFloat(dynamicBone.name + "Radius"), 369 | 370 | EditorPrefs.GetFloat(dynamicBone.name + "EndOffsetX"), 371 | EditorPrefs.GetFloat(dynamicBone.name + "EndOffsetY"), 372 | EditorPrefs.GetFloat(dynamicBone.name + "EndOffsetZ"), 373 | 374 | EditorPrefs.GetFloat(dynamicBone.name + "GravityX"), 375 | EditorPrefs.GetFloat(dynamicBone.name + "GravityY"), 376 | EditorPrefs.GetFloat(dynamicBone.name + "GravityZ"), 377 | 378 | EditorPrefs.GetFloat(dynamicBone.name + "ForceX"), 379 | EditorPrefs.GetFloat(dynamicBone.name + "ForceY"), 380 | EditorPrefs.GetFloat(dynamicBone.name + "ForceZ") 381 | ); 382 | 383 | // Registry garbage collect 384 | EditorPrefs.DeleteKey(dynamicBone.name + "UpdateRate"); 385 | EditorPrefs.DeleteKey(dynamicBone.name + "Damping"); 386 | EditorPrefs.DeleteKey(dynamicBone.name + "Elasticity"); 387 | EditorPrefs.DeleteKey(dynamicBone.name + "Stiffness"); 388 | EditorPrefs.DeleteKey(dynamicBone.name + "Inert"); 389 | EditorPrefs.DeleteKey(dynamicBone.name + "Radius"); 390 | 391 | EditorPrefs.DeleteKey(dynamicBone.name + "EndOffsetX"); 392 | EditorPrefs.DeleteKey(dynamicBone.name + "EndOffsetY"); 393 | EditorPrefs.DeleteKey(dynamicBone.name + "EndOffsetZ"); 394 | 395 | EditorPrefs.DeleteKey(dynamicBone.name + "GravityX"); 396 | EditorPrefs.DeleteKey(dynamicBone.name + "GravityY"); 397 | EditorPrefs.DeleteKey(dynamicBone.name + "GravityZ"); 398 | 399 | EditorPrefs.DeleteKey(dynamicBone.name + "ForceX"); 400 | EditorPrefs.DeleteKey(dynamicBone.name + "ForceY"); 401 | EditorPrefs.DeleteKey(dynamicBone.name + "ForceZ"); 402 | } 403 | EditorPrefs.DeleteKey("DynamicBoneStudioDataSaved"); 404 | } 405 | } 406 | 407 | /* Bone and preset scrollview */ 408 | _studioScrollPos = EditorGUILayout.BeginScrollView(_studioScrollPos); 409 | { 410 | foreach (var dynamicBone in _allDynamicBones) 411 | { 412 | if (dynamicBone == null) 413 | continue; 414 | 415 | GUILayout.BeginVertical(EditorStyles.helpBox); 416 | { 417 | GUILayout.BeginHorizontal(); 418 | EditorGUILayout.LabelField(dynamicBone.name, EditorStyles.boldLabel, new []{GUILayout.ExpandWidth(false)}); 419 | if (_isAskForBoneNameEnabled) 420 | { 421 | if (GUILayout.Button("Save as preset")) 422 | { 423 | var saveInstance = CreateInstance(); 424 | saveInstance.ShowUtility(); 425 | _isPresetNameSet = true; 426 | _presetSaveInstance = saveInstance; 427 | } 428 | } 429 | else 430 | { 431 | if (GUILayout.Button("Save as preset '" + dynamicBone.name + "'")) 432 | { 433 | SaveDynamicBonePreset(dynamicBone.name, dynamicBone); 434 | LoadDynamicBonePresets(); 435 | } 436 | } 437 | try 438 | { 439 | if (_isPresetNameSet && _presetSaveInstance.GetPresetName() != "") 440 | { 441 | //SO SCUFFED LMAO 442 | SaveDynamicBonePreset(_presetSaveInstance.PresetName, dynamicBone); 443 | _presetSaveInstance.PresetName = ""; 444 | _presetSaveInstance = null; 445 | _isPresetNameSet = false; 446 | LoadDynamicBonePresets(); 447 | } 448 | } 449 | catch (Exception ex) 450 | { 451 | Debug.Log(ex); 452 | } 453 | GUILayout.EndHorizontal(); 454 | GUILayout.BeginHorizontal(); 455 | if (_dynamicBonePresets.Count > 0) 456 | { 457 | EditorGUILayout.LabelField("Load preset:", new[] {GUILayout.ExpandWidth(false)}); 458 | _presetChoiceIndex = 459 | EditorGUILayout.Popup(_presetChoiceIndex, _dynamicBonePresets.Select(x=>x.Name) 460 | .ToArray()); 461 | if (GUILayout.Button("Load")) 462 | { 463 | var preset = LoadSingleDynamicBonePreset(_dynamicBonePresets.ElementAtOrDefault(_presetChoiceIndex).Name); 464 | if (preset != null) 465 | { 466 | UpdateDynamicBone(dynamicBone, true, dynamicBonePreset: preset); 467 | } 468 | else 469 | { 470 | Debug.Log("Preset was null"); 471 | } 472 | 473 | } 474 | if (_dynamicBonePresets.Count > 0 && GUILayout.Button("Delete")) 475 | { 476 | RemovePresetFromConfig(_dynamicBonePresets.ElementAtOrDefault(_presetChoiceIndex) 477 | .Name); 478 | _dynamicBonePresets.Remove( 479 | _dynamicBonePresets.ElementAtOrDefault(_presetChoiceIndex)); 480 | LoadDynamicBonePresets(); 481 | } 482 | } 483 | 484 | GUILayout.EndHorizontal(); 485 | UpdateDynamicBone(dynamicBone); 486 | } 487 | GUILayout.EndVertical(); 488 | } 489 | } 490 | 491 | EditorGUILayout.EndScrollView(); 492 | /* End bone and preset scrollview */ 493 | break; 494 | } 495 | } 496 | 497 | _isAboutShowing = EditorGUILayout.Foldout(_isAboutShowing, "About"); 498 | 499 | if (_isAboutShowing) 500 | { 501 | GUILayout.BeginVertical(EditorStyles.helpBox); 502 | 503 | EditorGUI.indentLevel++; 504 | EditorGUILayout.LabelField("Dynamic Bones Studio v0.3\n" + 505 | "by Kaori\n\n" + 506 | "Feedback or bugs can be posted to GitHub or sent to me through discord:\n" + 507 | "Kaori#0420", EditorStyles.textArea); 508 | GUILayout.BeginHorizontal(); 509 | GUILayout.Space(EditorGUI.indentLevel*15); 510 | 511 | if (GUILayout.Button("GitHub", new[] { GUILayout.ExpandWidth(false) })) 512 | { 513 | Application.OpenURL("https://github.com/kaaori/DynamicBonesStudio"); 514 | } 515 | 516 | GUILayout.EndHorizontal(); 517 | EditorGUI.indentLevel--; 518 | GUILayout.EndVertical(); 519 | } 520 | } 521 | 522 | private void RemovePresetFromConfig(string presetName) 523 | { 524 | if (presetName == null) 525 | { 526 | return; 527 | } 528 | if (_configFile == null) 529 | { 530 | InitConfigFile(); 531 | } 532 | _configFile.DeleteSection(presetName); 533 | } 534 | 535 | private List TryFindCommonAccessories() 536 | { 537 | var configFileWhitelist = LoadAccessoryList(); 538 | var prevAccessories = AccessoriesTransforms; 539 | if (_avatar == null) 540 | { 541 | return AccessoriesTransforms; 542 | } 543 | if (AccessoriesTransforms != null && AccessoriesTransforms.Count >= 0) 544 | { 545 | // Clean list of any missing items 546 | AccessoriesTransforms.RemoveAll(x => x == null); 547 | } 548 | foreach (var commonAccessory in _commonAccessories) 549 | { 550 | Debug.Log(commonAccessory); 551 | } 552 | var accessoriesTempList = _commonAccessories 553 | .Select(commonAccessory => 554 | AllBones.FirstOrDefault(x => x.name.ToLowerInvariant().Contains(commonAccessory))) 555 | .Where(tempAdd => tempAdd != null).ToList(); 556 | // Non-linq variant of ^ 557 | //var accessoriesTempList = new List(); 558 | //foreach (var commonAccessory in _commonAccessories) 559 | //{ 560 | // var tempAdd = AllBones.FirstOrDefault(x => x.name.ToLowerInvariant().Contains(commonAccessory.ToLowerInvariant())); 561 | // if (tempAdd != null) 562 | // { 563 | // accessoriesTempList.Add(tempAdd); 564 | // } 565 | //} 566 | 567 | if (accessoriesTempList.Any(x => x.name.ToLowerInvariant().Contains("ear")) && 568 | accessoriesTempList.Any(x => x.name.ToLowerInvariant().Contains("gimmick"))) 569 | { 570 | var ear = accessoriesTempList.FirstOrDefault(x => x.name.ToLowerInvariant().Contains("ear")); 571 | var gimmickL = accessoriesTempList.FirstOrDefault(x => x.name.ToLowerInvariant().Contains("gimmick_l")); 572 | var gimmickR = accessoriesTempList.FirstOrDefault(x => x.name.ToLowerInvariant().Contains("gimmick_r")); 573 | 574 | var useEar = EditorUtility.DisplayDialog("Potential Issue", 575 | "Found two potential ear root bones, which one would you like to apply the dynamic bone to?", 576 | ear.name, "Gimmick L & R"); 577 | 578 | if (useEar) 579 | { 580 | accessoriesTempList.Remove(gimmickL); 581 | accessoriesTempList.Remove(gimmickR); 582 | } 583 | else 584 | { 585 | accessoriesTempList.Remove(ear); 586 | } 587 | } 588 | if (prevAccessories != null) 589 | { 590 | accessoriesTempList = accessoriesTempList.Union(prevAccessories).ToList(); 591 | } 592 | return accessoriesTempList.Count > 0 ? accessoriesTempList : null; 593 | } 594 | 595 | private Transform TryFindHairRoot() 596 | { 597 | if (_avatar == null) 598 | { 599 | return null; 600 | } 601 | 602 | var hairTransform = _avatarAnim.GetBoneTransform(HumanBodyBones.Head).GetComponentsInChildren().FirstOrDefault(x=>x.name.ToLowerInvariant().Contains("hair")); 603 | if (hairTransform != null) 604 | { 605 | Debug.Log("Auto Dynamic Bones - Found transform named 'Hair' in children. Setting as default hair root"); 606 | return hairTransform; 607 | } 608 | return null; 609 | } 610 | 611 | public void AddDynamicBones() 612 | { 613 | LoadDynamicBonePresets(); 614 | if (_avatar.GetComponentsInChildren() != null) 615 | { 616 | var result = EditorUtility.DisplayDialog("Warning!", 617 | "Add dynamic bones now?\nNOTE: THIS WILL DELETE ALL EXISTING DYANMIC BONES, EVEN IN CHILDREN", "Yes", 618 | "No"); 619 | if (!result) 620 | { 621 | return; 622 | } 623 | Debug.Log("Destroying existing dynamic bones."); 624 | foreach (var dynamicBone in _avatar.gameObject.GetComponentsInChildren()) 625 | { 626 | DestroyImmediate(dynamicBone); 627 | } 628 | } 629 | 630 | if (_hairBone != null) 631 | { 632 | Debug.Log("Auto Dynamic Bones - Applying dynamic bones"); 633 | var hairDynamicBone = _hairBone.gameObject.AddComponent(); 634 | hairDynamicBone.m_Root = _hairBone; 635 | 636 | var hairPreset = _dynamicBonePresets.FirstOrDefault(x => x.Name == "Hair"); 637 | if (hairPreset != null) 638 | { 639 | EditorUtility.SetDirty(hairDynamicBone); 640 | UpdateDynamicBone(hairDynamicBone, true, 641 | dynamicBonePreset:_dynamicBonePresets.FirstOrDefault(x=>x.Name == "Hair")); 642 | } 643 | } 644 | 645 | if (AccessoriesTransforms != null) 646 | { 647 | Debug.Log("Auto Dynamic Bones - Adding accessory bones"); 648 | foreach (var accessory in AccessoriesTransforms) 649 | { 650 | var accessoryBone = accessory.gameObject.AddComponent(); 651 | accessoryBone.m_Root = accessory; 652 | } 653 | Debug.Log("Auto Dynamic Bones - Done!"); 654 | } 655 | 656 | } 657 | 658 | private DynamicBonesPreset LoadSingleDynamicBonePreset(string presetName) 659 | { 660 | if (_configFile == null) 661 | { 662 | InitConfigFile(); 663 | } 664 | if (presetName == null) 665 | { 666 | return null; 667 | } 668 | var section = _configFile.GetSection(presetName); 669 | var dynamicBonePreset = new DynamicBonesPreset(); 670 | var tempOffsetVec3 = Vector3.zero; 671 | var tempGravityVec3 = Vector3.zero; 672 | var tempForceVec3 = Vector3.zero; 673 | foreach (var sectionKey in section.Keys) 674 | { 675 | switch (sectionKey) 676 | { 677 | case "Name": 678 | dynamicBonePreset.Name = section[sectionKey]; 679 | break; 680 | case "UpdateRate": 681 | dynamicBonePreset.UpdateRate = float.Parse(section[sectionKey]); 682 | break; 683 | case "Damp": 684 | dynamicBonePreset.Damping = float.Parse(section[sectionKey]); 685 | break; 686 | case "Elasticity": 687 | dynamicBonePreset.Elasticity = float.Parse(section[sectionKey]); 688 | break; 689 | case "Stiff": 690 | dynamicBonePreset.Stiffness = float.Parse(section[sectionKey]); 691 | break; 692 | case "Inert": 693 | dynamicBonePreset.Inert = float.Parse(section[sectionKey]); 694 | break; 695 | case "Radius": 696 | dynamicBonePreset.Radius = float.Parse(section[sectionKey]); 697 | break; 698 | 699 | case "EndOffsetX": 700 | tempOffsetVec3.x = float.Parse(section[sectionKey]); 701 | break; 702 | case "EndOffsetY": 703 | tempOffsetVec3.y = float.Parse(section[sectionKey]); 704 | break; 705 | case "EndOffsetZ": 706 | tempOffsetVec3.z = float.Parse(section[sectionKey]); 707 | break; 708 | 709 | case "GravityX": 710 | tempGravityVec3.x = float.Parse(section[sectionKey]); 711 | break; 712 | case "GravityY": 713 | tempGravityVec3.y = float.Parse(section[sectionKey]); 714 | break; 715 | case "GravityZ": 716 | tempGravityVec3.z = float.Parse(section[sectionKey]); 717 | break; 718 | 719 | case "ForceX": 720 | tempForceVec3.x = float.Parse(section[sectionKey]); 721 | break; 722 | case "ForceY": 723 | tempForceVec3.y = float.Parse(section[sectionKey]); 724 | break; 725 | case "ForceZ": 726 | tempForceVec3.z = float.Parse(section[sectionKey]); 727 | break; 728 | 729 | } 730 | } 731 | dynamicBonePreset.EndOffset = tempOffsetVec3; 732 | dynamicBonePreset.Gravity = tempGravityVec3; 733 | dynamicBonePreset.Force = tempForceVec3; 734 | return dynamicBonePreset; 735 | } 736 | 737 | private void LoadDynamicBonePresets() 738 | { 739 | if (_configFile == null || !File.Exists(_cfgFilePath)) 740 | { 741 | InitConfigFile(); 742 | } 743 | 744 | _configFile.Refresh(); 745 | 746 | if (_configFile.SectionNames != null) 747 | { 748 | foreach (var configFileSectionName in _configFile.SectionNames) 749 | { 750 | if (configFileSectionName == "[AccessoryWhitelist]") 751 | { 752 | continue; 753 | } 754 | //var section = _configFile.GetSection(configFileSectionName); 755 | var dynamicBonePreset = LoadSingleDynamicBonePreset(configFileSectionName); 756 | if (dynamicBonePreset.Name != null) 757 | { 758 | _dynamicBonePresets.Add(dynamicBonePreset); 759 | } 760 | 761 | } 762 | } 763 | else 764 | { 765 | Debug.Log("No section names found"); 766 | } 767 | } 768 | 769 | private void UpdateDynamicBone(DynamicBone dynamicBone, bool isSetValue = false, float dampFloat = 0f, 770 | float elastFloat = 0f, float stiffFloat = 0f, 771 | float inertFloat = 0f, float radiusFloat = 0f, 772 | float endOffsetX = 0f, float endOffsetY = 0f, float endOffsetZ = 0f,// End offset vec3 773 | float gravityX = 0f, float gravityY = 0f, float gravityZ = 0f, // gravity vec3 774 | float forceX = 0f, float forceY = 0f, float forceZ = 0f, // force vec3 775 | float updateRateFloat = 0f, 776 | DynamicBonesPreset dynamicBonePreset = null) 777 | { 778 | var dynamicBoneTarget = dynamicBone; 779 | var dynamicBoneSo = new SerializedObject(dynamicBoneTarget); 780 | var root = dynamicBoneSo.FindProperty("m_Root"); 781 | var updateRate = dynamicBoneSo.FindProperty("m_UpdateRate"); 782 | var damp = dynamicBoneSo.FindProperty("m_Damping"); 783 | var elast = dynamicBoneSo.FindProperty("m_Elasticity"); 784 | var stiff = dynamicBoneSo.FindProperty("m_Stiffness"); 785 | var inert = dynamicBoneSo.FindProperty("m_Inert"); 786 | var radius = dynamicBoneSo.FindProperty("m_Radius"); 787 | var endLength = dynamicBoneSo.FindProperty("m_EndLength"); 788 | var endOffset = dynamicBoneSo.FindProperty("m_EndOffset"); 789 | var grav = dynamicBoneSo.FindProperty("m_Gravity"); 790 | var force = dynamicBoneSo.FindProperty("m_Force"); 791 | var colliders = dynamicBoneSo.FindProperty("m_Colliders"); 792 | var exclusions = dynamicBoneSo.FindProperty("m_Exclusions"); 793 | 794 | if (isSetValue) 795 | { 796 | if (dynamicBonePreset != null) 797 | { 798 | dampFloat = dynamicBonePreset.Damping; 799 | updateRateFloat = dynamicBonePreset.UpdateRate; 800 | elastFloat = dynamicBonePreset.Elasticity; 801 | stiffFloat = dynamicBonePreset.Stiffness; 802 | inertFloat = dynamicBonePreset.Inert; 803 | radiusFloat = dynamicBonePreset.Radius; 804 | 805 | endOffsetX = dynamicBonePreset.EndOffset.x; 806 | endOffsetY = dynamicBonePreset.EndOffset.y; 807 | endOffsetZ = dynamicBonePreset.EndOffset.z; 808 | 809 | gravityX = dynamicBonePreset.Gravity.x; 810 | gravityY = dynamicBonePreset.Gravity.y; 811 | gravityZ = dynamicBonePreset.Gravity.z; 812 | 813 | forceX = dynamicBonePreset.Force.x; 814 | forceY = dynamicBonePreset.Force.y; 815 | forceZ = dynamicBonePreset.Force.z; 816 | 817 | } 818 | updateRate.floatValue = updateRateFloat; 819 | damp.floatValue = dampFloat; 820 | elast.floatValue = elastFloat; 821 | stiff.floatValue = stiffFloat; 822 | inert.floatValue = inertFloat; 823 | radius.floatValue = radiusFloat; 824 | endOffset.vector3Value = new Vector3(endOffsetX, endOffsetY, endOffsetZ); 825 | grav.vector3Value = new Vector3(gravityX, gravityY, gravityZ); 826 | force.vector3Value = new Vector3(forceX, forceY, forceZ); 827 | } 828 | 829 | EditorGUI.indentLevel = 1; 830 | { 831 | EditorGUILayout.PropertyField(updateRate, true, new[]{GUILayout.ExpandWidth(false)}); 832 | EditorGUILayout.PropertyField(damp, true); 833 | EditorGUILayout.PropertyField(elast, true); 834 | EditorGUILayout.PropertyField(stiff, true); 835 | EditorGUILayout.PropertyField(inert, true); 836 | EditorGUILayout.PropertyField(radius, true); 837 | EditorGUILayout.PropertyField(endLength, true); 838 | 839 | EditorGUILayout.BeginHorizontal(); 840 | { 841 | EditorGUILayout.PropertyField(endOffset, true, new[] { GUILayout.ExpandWidth(false) }); 842 | EditorGUILayout.PropertyField(grav, true, new[] { GUILayout.ExpandWidth(false) }); 843 | } 844 | EditorGUILayout.EndHorizontal(); 845 | 846 | EditorGUILayout.PropertyField(force, true, new[] { GUILayout.ExpandWidth(false) }); 847 | 848 | EditorGUILayout.BeginHorizontal(); 849 | { 850 | EditorGUILayout.PropertyField(colliders, true, new[] { GUILayout.ExpandWidth(false) }); 851 | EditorGUILayout.PropertyField(exclusions, true, new[] { GUILayout.ExpandWidth(false) }); 852 | } 853 | EditorGUILayout.EndHorizontal(); 854 | 855 | } 856 | EditorGUI.indentLevel = 0; 857 | 858 | dynamicBoneSo.ApplyModifiedProperties(); 859 | } 860 | 861 | private void SaveDynamicBonePreset(string presetName, DynamicBone bone) 862 | { 863 | if (_configFile == null) 864 | { 865 | InitConfigFile(); 866 | } 867 | _configFile.Refresh(); 868 | 869 | _configFile.SetValue(presetName, "Name", presetName); 870 | _configFile.SetValue(presetName, "UpdateRate", bone.m_UpdateRate.ToString()); 871 | _configFile.SetValue(presetName, "Damp", bone.m_Damping.ToString()); 872 | _configFile.SetValue(presetName, "Elasticity", bone.m_Elasticity.ToString()); 873 | _configFile.SetValue(presetName, "Stiff", bone.m_Stiffness.ToString()); 874 | _configFile.SetValue(presetName, "Inert", bone.m_Inert.ToString()); 875 | _configFile.SetValue(presetName, "Radius", bone.m_Radius.ToString()); 876 | 877 | // End offset vec3 878 | _configFile.SetValue(presetName, "EndOffsetX", bone.m_EndOffset.x.ToString()); 879 | _configFile.SetValue(presetName, "EndOffsetY", bone.m_EndOffset.y.ToString()); 880 | _configFile.SetValue(presetName, "EndOffsetZ", bone.m_EndOffset.z.ToString()); 881 | 882 | // Gravity vec3 883 | _configFile.SetValue(presetName, "GravityX", bone.m_Gravity.x.ToString()); 884 | _configFile.SetValue(presetName, "GravityY", bone.m_Gravity.y.ToString()); 885 | _configFile.SetValue(presetName, "GravityZ", bone.m_Gravity.z.ToString()); 886 | 887 | // Force vec3 888 | _configFile.SetValue(presetName, "ForceX", bone.m_Force.x.ToString()); 889 | _configFile.SetValue(presetName, "ForceY", bone.m_Force.y.ToString()); 890 | _configFile.SetValue(presetName, "ForceZ", bone.m_Force.z.ToString()); 891 | } 892 | 893 | /** TODO Presets 894 | * - In "studio" tab 895 | * - Set of sliders for each "accessory" added 896 | * - Text box to name accessory (default to bone name) 897 | * - Save to preset button 898 | * - Load presets as a dropdown list to apply to any set of sliders 899 | */ 900 | private void InitConfigFile() 901 | { 902 | if (File.Exists(_cfgFilePath)) 903 | { 904 | _configFile = new IniFile(_cfgFilePath); 905 | } 906 | else 907 | { 908 | _configFile = new IniFile(); 909 | _configFile.Save(_cfgFilePath); 910 | } 911 | } 912 | 913 | private Dictionary LoadAccessoryList() 914 | { 915 | if (_configFile == null) 916 | { 917 | InitConfigFile(); 918 | } 919 | var accessoryWhitelist = _configFile.GetSection("AccessoryWhitelist"); 920 | if (accessoryWhitelist != null) 921 | { 922 | _commonAccessories = accessoryWhitelist.Values.ToList(); 923 | } 924 | return accessoryWhitelist; 925 | } 926 | } 927 | -------------------------------------------------------------------------------- /Editor/DynamicBonesStudioWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1fec74d7ae53fe143b5f0a81acb2cbbe 3 | timeCreated: 1519864856 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/IniFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | using System.IO; 6 | using System.Text.RegularExpressions; 7 | 8 | /// 9 | /// Read/Write .ini Files 10 | /// 11 | /// Version 1, 2009-08-15 12 | /// http://www.Stum.de 13 | /// Version 2, Modified 2018-03-11 14 | /// Kaori 15 | /// 16 | /// 17 | /// It supports the simple .INI Format: 18 | /// 19 | /// [SectionName] 20 | /// Key1=Value1 21 | /// Key2=Value2 22 | /// 23 | /// [Section2] 24 | /// Key3=Value3 25 | /// 26 | /// You can have empty lines (they are ignored), but comments are not supported 27 | /// Key4=Value4 ; This is supposed to be a comment, but will be part of Value4 28 | /// 29 | /// Whitespace is not trimmed from the beginning and end of either Key and Value 30 | /// 31 | /// Licensed under WTFPL 32 | /// http://sam.zoy.org/wtfpl/ 33 | /// 34 | public class IniFile 35 | { 36 | private Dictionary> _iniFileContent; 37 | public List SectionNames = new List(); 38 | private string Filename; 39 | private readonly Regex _sectionRegex = new Regex(@"(?<=\[)(?[^\]]+)(?=\])"); 40 | private readonly Regex _keyValueRegex = new Regex(@"(?[^=]+)=(?.+)"); 41 | 42 | public IniFile() : this(null) { } 43 | 44 | public IniFile(string filename) 45 | { 46 | _iniFileContent = new Dictionary>(); 47 | Filename = filename; 48 | if (filename != null) Load(filename); 49 | } 50 | 51 | public void Refresh() 52 | { 53 | if (Filename != "") 54 | { 55 | Load(Filename); 56 | } 57 | } 58 | 59 | /// 60 | /// Get a specific value from the .ini file 61 | /// 62 | /// 63 | /// 64 | /// The value of the given key in the given section, or NULL if not found 65 | public string GetValue(string sectionName, string key) 66 | { 67 | this.Refresh(); 68 | if (_iniFileContent.ContainsKey(sectionName) && _iniFileContent[sectionName].ContainsKey(key)) 69 | return _iniFileContent[sectionName][key]; 70 | else 71 | return null; 72 | } 73 | 74 | /// 75 | /// Set a specific value in a section 76 | /// 77 | /// 78 | /// 79 | /// 80 | public void SetValue(string sectionName, string key, string value) 81 | { 82 | if (!_iniFileContent.ContainsKey(sectionName)) _iniFileContent[sectionName] = new Dictionary(); 83 | _iniFileContent[sectionName][key] = value; 84 | 85 | this.Save(Filename); 86 | } 87 | 88 | public void DeleteValue(string sectionName, string key) 89 | { 90 | this.Refresh(); 91 | if (!_iniFileContent.ContainsKey(sectionName) && !_iniFileContent[sectionName].ContainsKey(key)) return; 92 | _iniFileContent.Remove(_iniFileContent[sectionName][key]); 93 | this.Save(Filename); 94 | } 95 | 96 | public void DeleteSection(string sectionName) 97 | { 98 | this.Refresh(); 99 | if (!_iniFileContent.ContainsKey(sectionName)) return; 100 | 101 | _iniFileContent[sectionName].Clear(); 102 | _iniFileContent.Remove(sectionName); 103 | 104 | this.Save(Filename); 105 | } 106 | 107 | /// 108 | /// Get all the Values for a section 109 | /// 110 | /// 111 | /// A Dictionary with all the Key/Values for that section (maybe empty but never null) 112 | public Dictionary GetSection(string sectionName) 113 | { 114 | if (_iniFileContent.ContainsKey(sectionName)) 115 | return new Dictionary(_iniFileContent[sectionName]); 116 | else 117 | return new Dictionary(); 118 | } 119 | 120 | /// 121 | /// Set an entire sections values 122 | /// 123 | /// 124 | /// 125 | public void SetSection(string sectionName, IDictionary sectionValues) 126 | { 127 | if (sectionValues == null) return; 128 | _iniFileContent[sectionName] = new Dictionary(sectionValues); 129 | 130 | this.Save(Filename); 131 | } 132 | 133 | 134 | /// 135 | /// Load an .INI File 136 | /// 137 | /// 138 | /// 139 | public bool Load(string filename) 140 | { 141 | if (File.Exists(filename)) 142 | { 143 | try 144 | { 145 | SectionNames.Clear(); 146 | var content = File.ReadAllLines(filename); 147 | _iniFileContent = new Dictionary>(); 148 | string currentSectionName = string.Empty; 149 | foreach (var line in content) 150 | { 151 | Match m = _sectionRegex.Match(line); 152 | if (m.Success) 153 | { 154 | currentSectionName = m.Groups["SectionName"].Value; 155 | SectionNames.Add(currentSectionName); 156 | //UnityEngine.Debug.Log("Added to section names"); 157 | } 158 | else 159 | { 160 | m = _keyValueRegex.Match(line); 161 | if (m.Success) 162 | { 163 | string key = m.Groups["Key"].Value; 164 | string value = m.Groups["Value"].Value; 165 | 166 | Dictionary kvpList; 167 | if (_iniFileContent.ContainsKey(currentSectionName)) 168 | { 169 | kvpList = _iniFileContent[currentSectionName]; 170 | } 171 | else 172 | { 173 | kvpList = new Dictionary(); 174 | } 175 | kvpList[key] = value; 176 | _iniFileContent[currentSectionName] = kvpList; 177 | } 178 | } 179 | } 180 | return true; 181 | } 182 | catch (Exception ex) 183 | { 184 | UnityEngine.Debug.Log(ex); 185 | return false; 186 | } 187 | 188 | } 189 | else 190 | { 191 | return false; 192 | } 193 | } 194 | 195 | /// 196 | /// Save the content of this class to an INI File 197 | /// 198 | /// 199 | /// 200 | public bool Save(string filename) 201 | { 202 | var sb = new StringBuilder(); 203 | if (_iniFileContent != null) 204 | { 205 | foreach (var sectionName in _iniFileContent) 206 | { 207 | sb.AppendFormat("[{0}]\r\n", sectionName.Key); 208 | foreach (var keyValue in sectionName.Value) 209 | { 210 | sb.AppendFormat("{0}={1}\r\n", keyValue.Key, keyValue.Value); 211 | } 212 | } 213 | } 214 | try 215 | { 216 | File.WriteAllText(filename, sb.ToString()); 217 | return true; 218 | } 219 | catch 220 | { 221 | return false; 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /Editor/IniFile.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: edfd740b29f74994ba388529c2844162 3 | timeCreated: 1520801431 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/SavePresetWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | class SavePresetWindow : EditorWindow 5 | { 6 | 7 | public string PresetName = ""; 8 | private bool _isClicked; 9 | 10 | void OnGUI() 11 | { 12 | PresetName = EditorGUILayout.TextField("Preset Name", PresetName); 13 | if (GUILayout.Button("Save Preset")) 14 | { 15 | OnClickSavePrefab(); 16 | GUIUtility.ExitGUI(); 17 | } 18 | } 19 | 20 | public string GetPresetName() 21 | { 22 | return _isClicked ? 23 | PresetName : ""; 24 | } 25 | 26 | void OnClickSavePrefab() 27 | { 28 | PresetName = PresetName.Trim(); 29 | _isClicked = true; 30 | if (string.IsNullOrEmpty(PresetName)) 31 | { 32 | EditorUtility.DisplayDialog("Unable to save Preset", "Please specify a valid Preset name.", "Close"); 33 | return; 34 | } 35 | Close(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /Editor/SavePresetWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1daad603aaa431846a984d23ab3701b8 3 | timeCreated: 1520803253 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamicBonesStudio 2 | A useful tool for quickly setting up and manipulating dynamic bones for Unity characters 3 | # Required unity assets 4 | * Dynamic Bone (ver>=1.2.0 ) 5 | 6 | # Getting started / How to use; 7 | 1. Open the Dynamic Bones Studio editor window. (Window -> Dynamic Bones Studio) 8 | 2. Add your avatar to the specified field, and correct the automatically-found hair root bone, if neccessary. 9 | 3. Click "Try and find common accessories" to attempt to find accessories configured in the .ini file 10 | 4. Click apply dynamic bones and confirm! 11 | 12 | # Studio tab; 13 | After applying your bones, you can switch to the studio tab to change the dynamic bone settings from one window. 14 | If you make some changes you want to save in play mode, simply click the save button and then once you exit play mode, click the load button that appears. 15 | 16 | # Presets; 17 | You can save and load presets for quick testing/configuring of avatars. 18 | 19 | ### To do so: 20 | 1. Go to the studio tab after adding dynamic bones. 21 | 2. Configure your bone to your preferred settings (NOTE: does NOT save colliders or exclusions at this time...) 22 | *Tick "Ask for name when saving dynamic bone presets" to customise the name they are saved as if desired.* 23 | 3. Click "Save as preset" 24 | 25 | ### To load presets; 26 | 1. Once a preset has been saved, click "refresh presets" if the drop down boxes do not appear. 27 | 2. Select the preset by name from the dropdown and click "load" 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6ca417570d800dc47a37502bede3b56a 3 | timeCreated: 1520280695 4 | licenseType: Free 5 | DefaultImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | --------------------------------------------------------------------------------