Initial Commit
This commit is contained in:
8
ReplaceMotion/Editor.meta
Normal file
8
ReplaceMotion/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b095e083c4f79b24aaf1e6460f3e35e8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
384
ReplaceMotion/Editor/ReplaceMotion.cs
Normal file
384
ReplaceMotion/Editor/ReplaceMotion.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using System.Linq;
|
||||
|
||||
namespace DreadScripts.ReplaceMotion
|
||||
{
|
||||
public class ReplaceMotion : EditorWindow
|
||||
{
|
||||
static List<Motion> originalMotion = new List<Motion>();
|
||||
private static bool hasEmptyState;
|
||||
private static bool replacingEmptyState;
|
||||
static bool[] replaceFields;
|
||||
static Motion[] targetMotion;
|
||||
static Motion emptyTargetMotion;
|
||||
static readonly Dictionary<Motion, Motion> replaceValues = new Dictionary<Motion, Motion>();
|
||||
|
||||
private static VRCAvatarDescriptor mainAvatar;
|
||||
private static AnimatorController mainController;
|
||||
|
||||
private static Vector2 scroll;
|
||||
|
||||
[MenuItem("DreadTools/Utility/Replace Motion")]
|
||||
private static void showWindow()
|
||||
{
|
||||
GetWindow<ReplaceMotion>(false, "Replace Motion", true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (!mainAvatar && !mainController)
|
||||
{
|
||||
mainAvatar = FindObjectOfType<VRCAvatarDescriptor>();
|
||||
if (mainAvatar)
|
||||
GetMotions(mainAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUIStyle labelButton = new GUIStyle(GUI.skin.button) { padding = new RectOffset(1, 1, 1, 1), margin = new RectOffset(), alignment = TextAnchor.MiddleCenter };
|
||||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Object dummy = CustomObjectField(new GUIContent("Target"), (Object)mainAvatar ?? mainController ?? null, true, true, out int resultType, typeof(VRCAvatarDescriptor), typeof(AnimatorController));
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
switch (resultType)
|
||||
{
|
||||
case -1:
|
||||
mainAvatar = null;
|
||||
mainController = null;
|
||||
break;
|
||||
case 0:
|
||||
mainAvatar = (VRCAvatarDescriptor)dummy;
|
||||
mainController = null;
|
||||
break;
|
||||
case 1:
|
||||
mainAvatar = null;
|
||||
mainController = (AnimatorController)dummy;
|
||||
break;
|
||||
}
|
||||
if (mainAvatar)
|
||||
GetMotions(mainAvatar);
|
||||
else if (mainController)
|
||||
GetMotions(mainController);
|
||||
}
|
||||
}
|
||||
EditorGUI.BeginDisabledGroup(!(mainAvatar || mainController));
|
||||
if (GUILayout.Button("Replace"))
|
||||
{
|
||||
PopulateDictionary();
|
||||
if (mainAvatar)
|
||||
SetMotions(mainAvatar);
|
||||
else
|
||||
SetMotions(mainController);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("<color=green>[Replace Motion] </color>Done!");
|
||||
|
||||
if (mainAvatar)
|
||||
GetMotions(mainAvatar);
|
||||
else
|
||||
GetMotions(mainController);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
|
||||
if (originalMotion.Count > 0 || hasEmptyState)
|
||||
{
|
||||
DrawSeperator();
|
||||
if (hasEmptyState)
|
||||
{
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
EditorGUILayout.ObjectField(null, typeof(Motion), false);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("->", "boldlabel", GUILayout.Width(20));
|
||||
GUILayout.Space(20);
|
||||
|
||||
if (!replacingEmptyState)
|
||||
replacingEmptyState = GUILayout.Toggle(replacingEmptyState, new GUIContent("Replace", "Replace all instances of the original motion"), "button");
|
||||
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("X", labelButton, GUILayout.Width(20)))
|
||||
replacingEmptyState = false;
|
||||
|
||||
emptyTargetMotion = (Motion)EditorGUILayout.ObjectField(emptyTargetMotion, typeof(Motion), true);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < originalMotion.Count; i++)
|
||||
{
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
EditorGUILayout.ObjectField(originalMotion[i], typeof(Motion), false);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("->", "boldlabel", GUILayout.Width(20));
|
||||
GUILayout.Space(20);
|
||||
if (!replaceFields[i])
|
||||
replaceFields[i] = GUILayout.Toggle(replaceFields[i],new GUIContent("Replace","Replace all instances of the original motion"), "button");
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("X", labelButton, GUILayout.Width(20)))
|
||||
replaceFields[i] = false;
|
||||
targetMotion[i] = (Motion)EditorGUILayout.ObjectField(targetMotion[i], typeof(Motion), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private static Object CustomObjectField(GUIContent label, Object displayObject, bool allowScene, bool checkComponents, out int resultTypeIndex, params System.Type[] validTypes)
|
||||
{
|
||||
//Special Cases
|
||||
bool supportsController = false;
|
||||
int controllerTypeIndex = -1;
|
||||
if (checkComponents)
|
||||
{
|
||||
for (int i = 0; i < validTypes.Length; i++)
|
||||
{
|
||||
if (validTypes[i] == typeof(AnimatorController))
|
||||
{
|
||||
supportsController = true;
|
||||
controllerTypeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Object dummy = EditorGUILayout.ObjectField(label, displayObject, typeof(Object), allowScene);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (!dummy)
|
||||
{
|
||||
resultTypeIndex = -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < validTypes.Length; i++)
|
||||
{
|
||||
if (dummy.GetType() == validTypes[i])
|
||||
{
|
||||
resultTypeIndex = i;
|
||||
return dummy;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkComponents && dummy is GameObject go)
|
||||
{
|
||||
Component[] components = go.GetComponents<Component>();
|
||||
for (int i = 0; i < components.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < validTypes.Length; j++)
|
||||
{
|
||||
if (components[i].GetType() == validTypes[j])
|
||||
{
|
||||
resultTypeIndex = j;
|
||||
return components[i];
|
||||
}
|
||||
|
||||
//Special Cases
|
||||
if (supportsController && (components[i] is Animator ani) && ani.runtimeAnimatorController)
|
||||
{
|
||||
resultTypeIndex = controllerTypeIndex;
|
||||
return AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(ani.runtimeAnimatorController));
|
||||
}
|
||||
///////////////
|
||||
}
|
||||
}
|
||||
}
|
||||
string validTypesMessage = string.Join(", ", validTypes.Select(t => t.Name));
|
||||
Debug.LogWarning("Field must be of Type: " + validTypesMessage);
|
||||
}
|
||||
resultTypeIndex = -2;
|
||||
return dummy;
|
||||
}
|
||||
private void SetMotions(VRCAvatarDescriptor avatar)
|
||||
{
|
||||
IterateStates(avatar, SetMotions);
|
||||
}
|
||||
|
||||
private void GetMotions(VRCAvatarDescriptor avatar)
|
||||
{
|
||||
GetStart();
|
||||
IterateStates(avatar, s => GetMotions(s.motion));
|
||||
GetFinal();
|
||||
}
|
||||
private void SetMotions(AnimatorController controller)
|
||||
{
|
||||
IterateStates(controller, SetMotions);
|
||||
}
|
||||
private void GetMotions(AnimatorController controller)
|
||||
{
|
||||
GetStart();
|
||||
IterateStates(controller, s => GetMotions(s.motion));
|
||||
GetFinal();
|
||||
}
|
||||
|
||||
private void GetStart()
|
||||
{
|
||||
hasEmptyState = false;
|
||||
emptyTargetMotion = null;
|
||||
originalMotion.Clear();
|
||||
}
|
||||
private void GetFinal()
|
||||
{
|
||||
originalMotion = originalMotion.Distinct().ToList();
|
||||
targetMotion = new Motion[originalMotion.Count];
|
||||
replaceFields = new bool[originalMotion.Count];
|
||||
}
|
||||
|
||||
private void GetMotions(BlendTree tree)
|
||||
{
|
||||
originalMotion.Add(tree);
|
||||
for (int i = 0; i < tree.children.Length; i++)
|
||||
{
|
||||
GetMotions(tree.children[i].motion);
|
||||
}
|
||||
}
|
||||
|
||||
private void GetMotions(Motion motion)
|
||||
{
|
||||
if (!motion)
|
||||
hasEmptyState = true;
|
||||
else
|
||||
{
|
||||
if (motion is AnimationClip)
|
||||
originalMotion.Add(motion);
|
||||
else
|
||||
{
|
||||
if (motion is BlendTree tree)
|
||||
{
|
||||
GetMotions(tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetMotions(AnimatorState state)
|
||||
{
|
||||
if (!state.motion)
|
||||
{
|
||||
if (replacingEmptyState)
|
||||
state.motion = emptyTargetMotion;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (replaceValues.ContainsKey(state.motion))
|
||||
{
|
||||
state.motion = replaceValues[state.motion];
|
||||
EditorUtility.SetDirty(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state.motion is BlendTree tree)
|
||||
{
|
||||
SetMotions(tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void SetMotions(BlendTree tree)
|
||||
{
|
||||
ChildMotion[] newMotions = tree.children;
|
||||
for (int i = 0; i < newMotions.Length; i++)
|
||||
{
|
||||
if (replaceValues.ContainsKey(newMotions[i].motion))
|
||||
newMotions[i].motion = replaceValues[newMotions[i].motion];
|
||||
else
|
||||
if (newMotions[i].motion is BlendTree subTree)
|
||||
SetMotions(subTree);
|
||||
}
|
||||
tree.children = newMotions;
|
||||
EditorUtility.SetDirty(tree);
|
||||
}
|
||||
|
||||
private void PopulateDictionary()
|
||||
{
|
||||
replaceValues.Clear();
|
||||
for (int i = 0; i < originalMotion.Count; i++)
|
||||
{
|
||||
if (!replaceFields[i])
|
||||
replaceValues.Add(originalMotion[i], originalMotion[i]);
|
||||
else
|
||||
replaceValues.Add(originalMotion[i], targetMotion[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void IterateStates(VRCAvatarDescriptor avatar, System.Action<AnimatorState> action)
|
||||
{
|
||||
HashSet<AnimatorController> visitedControllers = new HashSet<AnimatorController>();
|
||||
foreach (var layer in avatar.baseAnimationLayers.Concat(avatar.specialAnimationLayers))
|
||||
{
|
||||
if (layer.animatorController)
|
||||
{
|
||||
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(layer.animatorController));
|
||||
if (controller && !visitedControllers.Contains(controller))
|
||||
{
|
||||
IterateStates(controller, action);
|
||||
visitedControllers.Add(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var runtimeController in avatar.GetComponentsInChildren<Animator>().Select(a => a.runtimeAnimatorController))
|
||||
{
|
||||
if (runtimeController)
|
||||
{
|
||||
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(runtimeController));
|
||||
if (controller && !visitedControllers.Contains(controller))
|
||||
{
|
||||
IterateStates(controller, action);
|
||||
visitedControllers.Add(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void IterateStates(AnimatorController controller, System.Action<AnimatorState> action)
|
||||
{
|
||||
foreach (var layer in controller.layers)
|
||||
{
|
||||
IterateStates(layer.stateMachine, action, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void IterateStates(AnimatorStateMachine machine, System.Action<AnimatorState> action, bool deep = true)
|
||||
{
|
||||
if (deep)
|
||||
foreach (var subMachine in machine.stateMachines.Select(c => c.stateMachine))
|
||||
{
|
||||
IterateStates(subMachine, action);
|
||||
}
|
||||
|
||||
foreach (var state in machine.states.Select(s => s.state))
|
||||
{
|
||||
action(state);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSeperator()
|
||||
{
|
||||
Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(1 + 2));
|
||||
r.height = 1;
|
||||
r.y += 1;
|
||||
r.x -= 2;
|
||||
r.width += 6;
|
||||
ColorUtility.TryParseHtmlString(EditorGUIUtility.isProSkin ? "#595959" : "#858585", out Color lineColor);
|
||||
EditorGUI.DrawRect(r, lineColor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
11
ReplaceMotion/Editor/ReplaceMotion.cs.meta
Normal file
11
ReplaceMotion/Editor/ReplaceMotion.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20a6e7c15d5f4744681ced7247a029a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "com.dreadscripts.replacemotion.Editor",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 355ae8e34ea9b874a9965deb911d8a85
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
21
ReplaceMotion/LICENSE
Normal file
21
ReplaceMotion/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Dreadrith
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
ReplaceMotion/LICENSE.meta
Normal file
7
ReplaceMotion/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38e2ba75221516844b3416a99546dbe5
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
20
ReplaceMotion/README.md
Normal file
20
ReplaceMotion/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Replace Motion
|
||||
Easily replace all instances of a motion in an Avatar or Controller with the desired motion.
|
||||
|
||||

|
||||
|
||||
Replace Motion will easily replace all instances of a motion in an Avatar or Controller with the desired motion.
|
||||
Found under DreadTools > Utilities > Replace Motion
|
||||
|
||||
Window
|
||||
-------
|
||||
After specifying a target avatar or controller, the window will show you all the motions used. You can replace all instances of a motion with another motion of your choice.
|
||||
|
||||
Target:The Avatar or Animator Controller to replace motions in.
|
||||
|
||||
Motion List:
|
||||
- Left Field: The original motion that's used in the target
|
||||
- Right Field: the new motion that the original motion should be replaced with
|
||||
|
||||
### Thank you
|
||||
If you enjoy Replace Motion, please consider [supporting me ♡](https://ko-fi.com/Dreadrith)!
|
7
ReplaceMotion/README.md.meta
Normal file
7
ReplaceMotion/README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 575a36e847cb59c4488f4fc71997bec8
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
17
ReplaceMotion/package.json
Normal file
17
ReplaceMotion/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "com.dreadscripts.replacemotion",
|
||||
"displayName": "DreadScripts - ReplaceMotion",
|
||||
"version": "1.0.1",
|
||||
"description": "Easily replace all instances of a motion in an Avatar or Controller with the desired motion.",
|
||||
"gitDependencies": {},
|
||||
"vpmDependencies": {},
|
||||
"author": {
|
||||
"name": "Dreadrith",
|
||||
"email": "dreadscripts@gmail.com",
|
||||
"url": "https://www.dreadrith.com"
|
||||
},
|
||||
"legacyFolders": {},
|
||||
"legacyFiles": {},
|
||||
"type": "tool",
|
||||
"unity": "2019.4"
|
||||
}
|
7
ReplaceMotion/package.json.meta
Normal file
7
ReplaceMotion/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f83ab771cf8f96246b561c63d1a24535
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user