Skip to content

Instantly share code, notes, and snippets.

@aki-lua87
Last active April 9, 2021 20:14
Show Gist options
  • Save aki-lua87/7bc804bfda4f58c335060be4e8a06884 to your computer and use it in GitHub Desktop.
Save aki-lua87/7bc804bfda4f58c335060be4e8a06884 to your computer and use it in GitHub Desktop.
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDK3.Avatars.Components;
using UnityEditor.Animations;
using static VRC.SDK3.Avatars.Components.VRCAvatarDescriptor;
using UnityEditor;
using System.IO;
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control;
using VRC.SDKBase;
using System.Reflection;
using System.Text.RegularExpressions;
public class RadialInventory : EditorWindow
{
[MenuItem("Editor/RadialInventory")]
private static void Create()
{
GetWindow<RadialInventory>("RadialInventory");
}
private bool propGroup_IsOpen = true;
private SORadialInventory Values;
public Vector2 ScrollPosition = Vector2.zero;
public Stack<PropInfo> RemoveProps = new Stack<PropInfo>();
public Stack<GroupInfo> RemoveGroups = new Stack<GroupInfo>();
public VRCAvatarDescriptor beforeRoot = null;
public bool ShowAdvanceSettings = false;
private void OnGUI()
{
if (Values == null)
{
//初期化部分
Values = ScriptableObject.CreateInstance<SORadialInventory>();
}
using (var scrollScope = new EditorGUILayout.ScrollViewScope(ScrollPosition))
{
ScrollPosition = scrollScope.scrollPosition;
EditorGUILayout.LabelField("Settings");
EditorGUILayoutEx.Separator();
var generatedItemsPath = "";
using (new GUILayout.VerticalScope())
{
//GameObject類を設定する欄
//Descriptorとかを弄るアバター
Values.AvatarRoot = (VRCAvatarDescriptor)EditorGUILayout.ObjectField("AvatarRoot", Values.AvatarRoot, typeof(VRCAvatarDescriptor), true);
if (Values.AvatarRoot != null)
{
//AvatarRootが設定されたらいろいろ表示する
//作業用フォルダーの存在確認と生成
var name = string.Join("", Values.AvatarRoot.gameObject.name.ToArray().Where(c => !Path.GetInvalidFileNameChars().Contains(c))).Trim();
generatedItemsPath = "Assets/RadialInventory/GeneratedItems/" + name + "/";
if (!AssetDatabase.IsValidFolder(generatedItemsPath))
CreateFolderRecursively(generatedItemsPath);
}
else
{
Values.Groups.Clear();
beforeRoot = null;
}
//AvatarRootが変更されたら設定を復元
if (Values.AvatarRoot != beforeRoot)
{
if (Values.AvatarRoot != null)
{
SORadialInventory settings = (SORadialInventory)AssetDatabase.LoadAssetAtPath(generatedItemsPath + "SavedSettings.asset", typeof(SORadialInventory));
if (settings != null)
RestoreSettings(settings);
else
Values.Groups.Clear();
}
else
Values.Groups.Clear();
beforeRoot = Values.AvatarRoot;
}
Values.UseWriteDefaults = EditorGUILayout.Toggle("WriteDefaults", Values.UseWriteDefaults);
Values.SaveParameter = EditorGUILayout.Toggle("SaveParameter", Values.SaveParameter);
if (Values != null && Values.AvatarRoot != null && Values.AvatarRoot.baseAnimationLayers != null &&
Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null)
{
var controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController;
var layers = controller.layers.Where(n => !n.name.StartsWith("RILayerG"));
foreach(var layer in layers)
{
if(layer.stateMachine != null)
{
if(layer.stateMachine.states.Any(n => n.state.writeDefaultValues != Values.UseWriteDefaults))
{
EditorGUILayout.HelpBox("WriteDefaultsがFXレイヤー内で統一されていません。\n" +
"このままでも動作はしますが、表情切り替えにバグが発生する場合があります。\n" +
"WriteDefaultsのチェックを切り替えてもエラーメッセージが消えない場合は\n" +
"使用している他のアバターギミックなどを確認してみてください。", MessageType.Warning);
break;
}
}
}
}
}
EditorGUILayoutEx.Separator();
EditorGUILayout.LabelField("");
using (new GUILayout.HorizontalScope())
{
propGroup_IsOpen = EditorGUILayout.Foldout(propGroup_IsOpen, "PropGroup");
ShowAdvanceSettings = EditorGUILayout.Toggle(" Show Adv. Settings", ShowAdvanceSettings, GUILayout.MaxWidth(180f));
}
if (propGroup_IsOpen)
{
//PropGroupの一覧
EditorGUILayoutEx.Separator();
using (new GUILayout.VerticalScope())
{
int group_index = 0;
foreach (var group in Values.Groups)
{
using (new GUILayout.VerticalScope(GUI.skin.box))
{
using (new GUILayout.HorizontalScope())
{
Values.Groups[group_index].GroupName = EditorGUILayout.TextField(Values.Groups[group_index].GroupName, GUILayout.MinWidth(40f));
EditorGUILayout.LabelField(" Icon", GUILayout.Width(40f));
Values.Groups[group_index].GroupIcon = (Texture2D)EditorGUILayout.ObjectField(Values.Groups[group_index].GroupIcon, typeof(Texture2D), false);
EditorGUILayout.LabelField("IsExclusive", GUILayout.Width(80f));
Values.Groups[group_index].ExclusiveMode = EditorGUILayout.Toggle(Values.Groups[group_index].ExclusiveMode, GUILayout.Width(30f));
if (Values.Groups.Count > 1)
{
//PropGroupの削除ボタン(ここで削除するとエラーが発生するから後でまとめて削除)
//PropGroupが1個以下のときは非表示
if (GUILayout.Button("-", GUILayout.Width(20f)))
{
RemoveGroups.Push(group);
}
}
++group_index;
}
EditorGUILayoutEx.Separator();
using (new GUILayout.VerticalScope())
{
//PropGroup内のPropの一覧
int prop_index = 0;
foreach (var prop in group.Props)
{
using (new GUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Prop" + (++prop_index).ToString(), GUILayout.Width(50f));
prop.TargetObject = (GameObject)EditorGUILayout.ObjectField(prop.TargetObject, typeof(GameObject), true, GUILayout.MinWidth(10f));
EditorGUILayout.LabelField("Icon", GUILayout.Width(40f));
prop.PropIcon = (Texture2D)EditorGUILayout.ObjectField(prop.PropIcon, typeof(Texture2D), false);
if(ShowAdvanceSettings)
{
EditorGUILayout.LabelField("CustomAnim", GUILayout.Width(80f));
prop.CustomAnim = (AnimationClip)EditorGUILayout.ObjectField(prop.CustomAnim, typeof(AnimationClip), false, GUILayout.MinWidth(10f));
}
EditorGUILayout.LabelField("Default", GUILayout.Width(50f));
prop.IsDefaultEnabled = EditorGUILayout.Toggle(prop.IsDefaultEnabled, GUILayout.Width(30f));
EditorGUILayout.LabelField("Local", GUILayout.Width(40f));
prop.LocalOnly = EditorGUILayout.Toggle(prop.LocalOnly, GUILayout.Width(30f));
if (group.Props.Count > 1)
{
//PropGroup内のPropの削除ボタン(ここで削除するとエラーが発生するから後でまとめて削除)
//Propが1個以下のときは非表示
if (GUILayout.Button("-", GUILayout.Width(20f)))
{
RemoveProps.Push(prop);
}
}
}
}
while (RemoveProps.Any())
group.Props.Remove(RemoveProps.Pop());
if (group.Props.Count < (group.ExclusiveMode ? 8 : 7))
{
//Propの追加ボタン
//8個以上のときは非表示
if (GUILayout.Button("+"))
{
group.Props.Add(new PropInfo());
}
}
}
EditorGUILayoutEx.Separator();
}
}
while (RemoveGroups.Any())
{
Values.Groups.Remove(RemoveGroups.Pop());
}
if (Values.Groups.Count < 8)
{
//PropGroupの追加ボタン
//8個以上のときは非表示
if (GUILayout.Button("+"))
{
var list = new GroupInfo();
list.Props.Add(new PropInfo());
Values.Groups.Add(list);
list.GroupName = "Group" + Values.Groups.Count.ToString();
}
}
}
EditorGUILayoutEx.Separator();
}
EditorGUILayout.LabelField("");
List<string> errorTxt = new List<string>();
if (Values.AvatarRoot != null)
{
//適用条件を満たしているかのチェック
var hasNullProp = Values.Groups.Any(group => group.Props.Count(prop => prop.TargetObject == null) > 0);
var hasOverGroup = (Values.Groups.Count > 8) || Values.Groups.Any(n => n.Props.Count > (n.ExclusiveMode ? 8 : 7));
var hasNullGroup = Values.Groups.Any(n => n.Props.Count == 0);
if (Values.Groups.Count <= 0)
errorTxt.Add("グループが登録されていません。");
if (hasNullProp)
errorTxt.Add("GameObjectが設定されていないPropがあります。");
if (hasOverGroup)
errorTxt.Add("Propの数が7つ(ExclusiveModeでは8つ)以上あるグループがあります。");
if (hasNullGroup)
errorTxt.Add("Propが登録されていないグループがあります。");
if (GUILayout.Button(SystemStrings.RemoveAllGeneratedObjectsButtonText))
{
RemoveAllGeneratedObjects();
}
}
else
errorTxt.Add("AvatarRootが登録されていません。");
EditorGUILayoutEx.Separator();
//適用ボタン
//PropGroupとPropの数が1個以上 GameObjectが適切に設定されているときだけ表示
if (errorTxt.Count > 0)
{
using (new GUILayout.VerticalScope(GUI.skin.box))
{
foreach (var text in errorTxt)
EditorGUILayout.HelpBox(text, MessageType.Error);
}
}
else if(GUILayout.Button(SystemStrings.ApplyButtonText))
{
//設定を保存
SaveSettingsToFile(generatedItemsPath);
//アバターに適用
ApplyToAvatar(generatedItemsPath);
}
}
}
public void RemoveAllGeneratedObjects()
{
AnimatorController controller = null;
if (Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null)
{
controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController;
int ofs = 0;
var max = controller.layers.Count();
foreach (var layerIndex in Enumerable.Range(0, max))
{
var name = controller.layers[layerIndex + ofs].name;
if (name.StartsWith("RadInvLayer") || name.StartsWith("RadBitLayer"))
{
controller.RemoveLayer(layerIndex + ofs);
ofs--;
}
}
ofs = 0;
max = controller.parameters.Count();
foreach (var paramIndex in Enumerable.Range(0, max))
{
var name = controller.parameters[paramIndex + ofs].name;
if (name.StartsWith("RadInvStatus"))
{
controller.RemoveParameter(paramIndex + ofs);
ofs--;
}
}
}
if (Values.AvatarRoot.expressionParameters != null)
{
var expressionParam = Values.AvatarRoot.expressionParameters;
foreach (var v in Enumerable.Range(0, expressionParam.parameters.Count()))
{
if (expressionParam.parameters[v].name.StartsWith("RadInvStatus"))
{
expressionParam.parameters[v].name = "";
expressionParam.parameters[v].valueType = VRCExpressionParameters.ValueType.Int;
}
}
}
if (Values.AvatarRoot.expressionsMenu != null)
{
var menu = Values.AvatarRoot.expressionsMenu;
var ofs = 0;
foreach (var v in Enumerable.Range(0, menu.controls.Count()))
{
if (menu.controls[v].name == "RadialInventory")
{
menu.controls.RemoveAt(v + ofs);
ofs--;
}
}
}
}
public void GetFromV1(string generatedItemsPath)
{
Values.Groups.Clear();
var rootPath = GetGameObjectPath(Values.AvatarRoot.gameObject);
foreach (var v in Enumerable.Range(1, 8))
{
AnimationClip group_default = (AnimationClip)AssetDatabase.LoadAssetAtPath(generatedItemsPath + "Animations/" + "G" + v.ToString() + "DEFAULT.anim", typeof(AnimationClip));
if (group_default != null)
{
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(group_default);
var groupInfo = new GroupInfo();
groupInfo.GroupName = "Group" + (Values.Groups.Count + 1).ToString();
groupInfo.ExclusiveMode = true;
foreach (var bind in bindings)
{
AnimationCurve curve = AnimationUtility.GetEditorCurve(group_default, bind);
var obj = FindGameObjectFromPath(bind.path, Values.AvatarRoot.gameObject);
if (obj != null && curve != null && curve.keys.Length > 0)
{
var defaultStatus = Convert.ToBoolean((int)curve.keys[0].value);
var propInfo = new PropInfo();
propInfo.TargetObject = obj;
propInfo.IsDefaultEnabled = defaultStatus;
groupInfo.Props.Add(propInfo);
}
}
Values.Groups.Add(groupInfo);
}
}
}
public void ApplyToAvatar(string generatedItemsPath)
{
//VRCExpressionParametersの取得or新規作成
VRCExpressionParameters expressionParameters = Values.AvatarRoot.expressionParameters;
if (Values.AvatarRoot.expressionParameters == null)
{
var param = CreateExpressionParameters(generatedItemsPath + "RadInvExpressionParameters.asset");
expressionParameters = param;
Values.AvatarRoot.expressionParameters = expressionParameters;
}
if (!CheckHasParameterSpace(expressionParameters, Values.Groups.Count))
{
EditorUtility.DisplayDialog("Radial Inventory System", "エラー : Parametersに空きがありません", "OK");
return;
}
//AnimatorControllerの取得or新規作成
AnimatorController controller = GetOrCreateAnimator(generatedItemsPath);
//自動生成されたAnimationsフォルダを一旦削除
ReCreateFolder(generatedItemsPath + "Animations");
//メインメニューの取得or新規作成
var risMainMenu = GetOrCreateAsset(generatedItemsPath + "RadInvMainMenu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu;
risMainMenu.controls = new List<VRCExpressionsMenu.Control>();
//RISメインメニューを追加するルートメニューを取得or新規作成
VRCExpressionsMenu addDestination;
if (Values.AvatarRoot.expressionsMenu != null)
addDestination = Values.AvatarRoot.expressionsMenu;
else
{
var rootMenu = GetOrCreateAsset(generatedItemsPath + "ExpressionsRootMenu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu;
rootMenu.controls = new List<VRCExpressionsMenu.Control>();
Values.AvatarRoot.expressionsMenu = addDestination = rootMenu;
}
//ルートメニューにRISメインメニューを追加
var control = GetOrCreateMenuControl(addDestination, "RadialInventory", ControlType.SubMenu);
Texture2D icon = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/RadialInventory/icon.png", typeof(Texture2D));
control.icon = icon;
control.subMenu = risMainMenu;
Dictionary<string, AnimationClip> objectToggleClips = new Dictionary<string, AnimationClip>();
AnimatorState state;
AnimationClip clip;
List<AnimatorState[]> states = new List<AnimatorState[]>();
ClearAllGeneratedStateMachine(controller);
var containLocalItem = Values.Groups.Any(group => group.Props.Any(prop => prop.LocalOnly));
var isLocalParamName = "IsLocal";
var containParameter = controller.parameters.Any(n => n.name == isLocalParamName);
if (containLocalItem && !containParameter)
controller.AddParameter(isLocalParamName, AnimatorControllerParameterType.Bool);
foreach (var groupIndex in Enumerable.Range(1, Values.Groups.Count))
{
states.Clear();
var group = Values.Groups[groupIndex - 1];
var clipOFF = new AnimationClip();
var paramName = "RadInvStatusG" + groupIndex.ToString();
//RIS用のParameterが存在しなかったら追加する(VRCExpressionParametersとControllerのParametersへ)
if (!CheckParameter(expressionParameters, paramName, controller, Values.SaveParameter))
{
EditorUtility.DisplayDialog("Radial Inventory System", "エラー : Parameterの追加に失敗しました", "OK");
return;
}
EditorUtility.SetDirty(expressionParameters);
//RIS用のLayerを取得/生成する
AnimatorControllerLayer layer = GetOrCreateControllerLayer(controller, "RadInvLayerG" + groupIndex.ToString());
//保存するためにレイヤーをサブアセットへ追加
AnimatorStateMachine stateMachine = layer.stateMachine;
if (!AssetDatabase.IsSubAsset(stateMachine))
AssetDatabase.AddObjectToAsset(stateMachine, controller);
stateMachine.hideFlags = HideFlags.HideInHierarchy;
stateMachine.name = "RILayerG" + groupIndex.ToString();
//ビットマスクの生成
var propCount = group.Props.Count;
var valueBase = 1 << propCount;
UpdateProgressBar("Creating bitmask.", 0, 1);
var bitMask = 1;
foreach (var v in Enumerable.Range(1, (propCount - 1)))
bitMask |= 1 << v;
var bitMaskRange = Enumerable.Range(0, bitMask + 1);
Debug.Log("BitMask : " + Convert.ToString(bitMask, 2).PadLeft(propCount, '0'));
Debug.Log("BitMaskCount : " + bitMaskRange.Count().ToString());
var clipName = "";
//Prop切り替え用メニューを取得(存在しなければ生成)
//Groupの数が1つだけなら生成しない
VRCExpressionsMenu propsMenu;
var targetParam = new VRCExpressionsMenu.Control.Parameter() { name = paramName };
if (Values.Groups.Count > 1 && group.Props.Count > 1)
{
propsMenu = GetOrCreateAsset(generatedItemsPath + "RadInvG" + groupIndex.ToString() + "Menu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu;
propsMenu.controls = new List<VRCExpressionsMenu.Control>();
//Groupの切り替えメニューを取得(存在しなければ生成)
//取得した上で現在のGroupをメニューに追加
var menuName = string.IsNullOrEmpty(Values.Groups[groupIndex - 1].GroupName) ? "Group" + groupIndex.ToString() : Values.Groups[groupIndex - 1].GroupName;
var groupControl = GetOrCreateMenuControl(risMainMenu, menuName, ControlType.SubMenu);
groupControl.subMenu = propsMenu;
groupControl.icon = group.GroupIcon;
EditorUtility.SetDirty(risMainMenu);
}
else
{
propsMenu = risMainMenu;
}
List<Dictionary<EditorCurveBinding, Vector2>> keyFramePairs = new List<Dictionary<EditorCurveBinding, Vector2>>();
//x -> off value, y -> on value
foreach (var v in group.Props)
{
var dict = new Dictionary<EditorCurveBinding, Vector2>();
if (v.CustomAnim == null)
dict.Add(new EditorCurveBinding() { propertyName = "m_IsActive", path = GetGameObjectPath(v.TargetObject, Values.AvatarRoot.gameObject), type = v.TargetObject.GetType() }, new Vector2(0, 1));
else
{
AnimationClip group_default = v.CustomAnim;
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(group_default);
foreach (var bind in bindings)
{
AnimationCurve curve = AnimationUtility.GetEditorCurve(group_default, bind);
if (curve != null && curve.keys.Length >= 2)
dict.Add(bind, new Vector2(curve.keys[0].value, curve.keys[1].value));
}
}
keyFramePairs.Add(dict);
}
if (!group.ExclusiveMode)
{
if (group.Props.Count > 1)
{
//排他モードでないならProp切り替え用メニューにデフォルトボタンを追加
control = GetOrCreateMenuControl(propsMenu, "Default", ControlType.Toggle);
control.parameter = targetParam;
control.value = valueBase;
Texture2D deficon = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/RadialInventory/reload.png", typeof(Texture2D));
control.icon = deficon;
}
}
else
{
state = null;
//排他モードならデフォルトステートを追加
clip = CreateExclusivePropAnim(group.Props, null, keyFramePairs, true);
clipName = "G" + groupIndex.ToString() + "DEFAULT";
objectToggleClips.Add(clipName, clip);
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim");
EditorUtility.SetDirty(clip);
state = stateMachine.AddState(clipName, new Vector2(-240, 120 + 60));
state.motion = clip;
state.writeDefaultValues = Values.UseWriteDefaults;
state.speed = 0;
state.cycleOffset = 0;
states.Add(new AnimatorState[] { state });
}
foreach (var propIndex in Enumerable.Range(1, propCount))
{
UpdateProgressBar("Creating AnimationClip.", propIndex, propCount);
var prop = group.Props[propIndex - 1];
if (group.ExclusiveMode)
{
//現在のPropをONにするアニメーションを作成
clip = CreateExclusivePropAnim(group.Props, prop, keyFramePairs);
clipName = "G" + groupIndex.ToString() + "P" + propIndex.ToString() + "ON";
objectToggleClips.Add(clipName, clip);
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim");
EditorUtility.SetDirty(clip);
state = stateMachine.AddState(clipName, new Vector2(0, 120 + 60 * propIndex));
state.motion = clip;
state.writeDefaultValues = Values.UseWriteDefaults;
state.speed = 0;
state.cycleOffset = 0;
if (prop.LocalOnly)
{
clip = new AnimationClip();
foreach (var prop_subIndex in Enumerable.Range(0, propCount))
{
var prop_sub = group.Props[prop_subIndex];
var keyframes = keyFramePairs[prop_subIndex];
foreach (var v1 in keyFramePairs)
{
foreach (var v2 in v1)
{
var curve = new AnimationCurve();
float status;
if (prop == prop_sub && prop.IsDefaultEnabled)
status = v2.Value.y;
else
status = v2.Value.x;
curve.AddKey(new Keyframe(0f, status));
curve.AddKey(new Keyframe(1f / clip.frameRate, status));
clip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, curve);
}
}
}
clipName = "G" + groupIndex.ToString() + "P" + propIndex.ToString() + "ON_REMOTE";
objectToggleClips.Add(clipName, clip);
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim");
EditorUtility.SetDirty(clip);
var state2 = stateMachine.AddState(clipName, new Vector2(0, -(120 + 60 * propIndex)));
state2.motion = clip;
state2.writeDefaultValues = Values.UseWriteDefaults;
state2.speed = 0;
state2.cycleOffset = 0;
states.Add(new AnimatorState[] { state, state2 });
}
else
states.Add(new AnimatorState[] { state });
}
//Prop切り替えメニューに現在のPropを追加
control = GetOrCreateMenuControl(propsMenu, prop.TargetObject.name, ControlType.Toggle);
control.parameter = targetParam;
control.value = propIndex + (group.ExclusiveMode ? 0 : valueBase);
control.icon = prop.PropIcon;
}
//各ステートから各ステートへ移るトランジションを作成
if (group.ExclusiveMode)
{
foreach (var destIndex in Enumerable.Range(0, propCount + 1))
{
var destState = states[destIndex];
if (destState.Length == 1)
{
MakeAnyStateTransition(stateMachine, destState[0], paramName, destIndex, propCount).canTransitionToSelf = false;
}
else
{
var transitonLocal = MakeAnyStateTransition(stateMachine, destState[0], paramName, destIndex, propCount);
transitonLocal.AddCondition(AnimatorConditionMode.If, 1, "IsLocal");
transitonLocal.canTransitionToSelf = false;
transitonLocal = MakeAnyStateTransition(stateMachine, destState[1], paramName, destIndex, propCount);
transitonLocal.AddCondition(AnimatorConditionMode.IfNot, 1, "IsLocal");
transitonLocal.canTransitionToSelf = false;
}
}
}
else
{
var bitstateClip = new AnimationClip();
float bitstateStatus = 0;
AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(bitstateClip);
settings.loopTime = true;
AnimationUtility.SetAnimationClipSettings(bitstateClip, settings);
clipName = "G" + groupIndex.ToString() + "STATES";
AssetDatabase.CreateAsset(bitstateClip, generatedItemsPath + "Animations/" + clipName + ".anim");
var remoteBitstateClip = new AnimationClip();
settings = AnimationUtility.GetAnimationClipSettings(remoteBitstateClip);
settings.loopTime = true;
AnimationUtility.SetAnimationClipSettings(remoteBitstateClip, settings);
clipName = "G" + groupIndex.ToString() + "STATES_REMOTE";
AssetDatabase.CreateAsset(remoteBitstateClip, generatedItemsPath + "Animations/" + clipName + ".anim");
var bitPatternStates = new List<AnimatorState[]>();
var propRange = Enumerable.Range(0, propCount);
var localPropBitMasks = new List<int>();
foreach(var v in propRange)
{
if (group.Props[v].LocalOnly)
localPropBitMasks.Add(1 << v);
}
//各ビット状態に応じたステートの追加
foreach (var bitPattern in bitMaskRange)
{
var xPos = 240 * (bitPattern % propCount + 1);
var yPos = 60 * (bitPattern / propCount + 3);
state = stateMachine.AddState(Convert.ToString(bitPattern, 2).PadLeft(propCount, '0'), new Vector2(xPos, yPos));
state.motion = bitstateClip;
state.writeDefaultValues = Values.UseWriteDefaults;
state.speed = 0;
state.cycleOffset = 1f / (bitMask + 1) * bitPattern;
var driver = state.AddStateMachineBehaviour<VRCAvatarParameterDriver>();
driver.parameters = new List<VRC_AvatarParameterDriver.Parameter>
{
new VRC_AvatarParameterDriver.Parameter { name = paramName, value = bitPattern }
};
if (localPropBitMasks.Any(n => (n & bitPattern) != 0))
{
var stateRemote = stateMachine.AddState(Convert.ToString(bitPattern, 2).PadLeft(propCount, '0') + "_REMOTE", new Vector2(xPos, -yPos));
stateRemote.motion = remoteBitstateClip;
stateRemote.writeDefaultValues = Values.UseWriteDefaults;
stateRemote.speed = 0;
stateRemote.cycleOffset = 1f / (bitMask + 1) * bitPattern;
driver = stateRemote.AddStateMachineBehaviour<VRCAvatarParameterDriver>();
driver.parameters = new List<VRC_AvatarParameterDriver.Parameter>
{
new VRC_AvatarParameterDriver.Parameter { name = paramName, value = bitPattern }
};
bitPatternStates.Add(new AnimatorState[] { state, stateRemote });
}
else
bitPatternStates.Add(new AnimatorState[] { state });
}
var propIndexRange = Enumerable.Range(1, propCount);
var bitstateCurves = new List<List<AnimationCurve>>();
foreach (var propIndex in propIndexRange)
bitstateCurves.Add(Enumerable.Range(0, keyFramePairs[propIndex - 1].Count).Select(n => new AnimationCurve()).ToList());
var remoteBitstateCurves = new List<List<AnimationCurve>>();
foreach (var propIndex in propIndexRange)
remoteBitstateCurves.Add(Enumerable.Range(0, keyFramePairs[propIndex - 1].Count).Select(n => new AnimationCurve()).ToList());
//Parameterがビットパターン数+Prop番号になった時にProp番号のビットを反転させ、そのステートに移るトランジションを作成。
var anyStateTransition = MakeAnyStateTransition(stateMachine, bitPatternStates[0][0], paramName, valueBase, propCount, AnimatorConditionMode.Equals, false, -valueBase);
anyStateTransition.canTransitionToSelf = false;
foreach (var bitPattern in bitMaskRange)
{
var bitPatternState = bitPatternStates[bitPattern][0];
anyStateTransition = stateMachine.AddAnyStateTransition(bitPatternState);
anyStateTransition.name = "->" + bitPatternState.name;
anyStateTransition.canTransitionToSelf = false;
anyStateTransition.AddCondition(AnimatorConditionMode.Equals, bitPattern, paramName);
if (bitPatternStates[bitPattern].Length == 2)
{
anyStateTransition.AddCondition(AnimatorConditionMode.If, 1, "IsLocal");
bitPatternState = bitPatternStates[bitPattern][1];
anyStateTransition = stateMachine.AddAnyStateTransition(bitPatternState);
anyStateTransition.name = "->" + bitPatternState.name;
anyStateTransition.canTransitionToSelf = false;
anyStateTransition.AddCondition(AnimatorConditionMode.Equals, bitPattern, paramName);
anyStateTransition.AddCondition(AnimatorConditionMode.IfNot, 1, "IsLocal");
}
foreach (var srcState in bitPatternStates[bitPattern])
{
foreach (var propIndex in propIndexRange)
{
var propBit = 1 << (propIndex - 1);
var xorBit = bitPattern ^ propBit;
var destIsLocal = bitPatternStates[xorBit].Length == 2;
if (destIsLocal)
{
MakeTransition(srcState, bitPatternStates[xorBit][0], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase)
.AddCondition(AnimatorConditionMode.If, 1f, "IsLocal");
MakeTransition(srcState, bitPatternStates[xorBit][1], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase)
.AddCondition(AnimatorConditionMode.IfNot, 1f, "IsLocal");
}
else
MakeTransition(srcState, bitPatternStates[xorBit][0], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase);
var pairs = keyFramePairs[propIndex - 1].ToList();
foreach (var v in Enumerable.Range(0, pairs.Count))
{
var pair = pairs[v];
var prop = group.Props[propIndex - 1];
var isActive = (bitPattern & propBit) != 0;
if (isActive ^ prop.IsDefaultEnabled)
bitstateStatus = pair.Value.y;
else
bitstateStatus = pair.Value.x;
bitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * bitPattern, bitstateStatus));
if (prop.LocalOnly)
bitstateStatus = prop.IsDefaultEnabled ? pair.Value.y : pair.Value.x;
remoteBitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * bitPattern, bitstateStatus));
}
}
}
}
foreach (var propIndex in propIndexRange)
{
var keyframes = keyFramePairs[propIndex - 1].ToList();
foreach (var v in Enumerable.Range(0, keyframes.Count))
{
bitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * (bitMask + 1), 1));
remoteBitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * (bitMask + 1), 1));
var v2 = keyframes[v];
bitstateClip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, bitstateCurves[propIndex - 1][v]);
remoteBitstateClip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, remoteBitstateCurves[propIndex - 1][v]);
}
}
EditorUtility.SetDirty(bitstateClip);
}
EditorUtility.SetDirty(propsMenu);
}
EditorUtility.ClearProgressBar();
Values.AvatarRoot.customExpressions = true;
Values.AvatarRoot.customizeAnimationLayers = true;
Values.AvatarRoot.baseAnimationLayers[4] = new CustomAnimLayer() { animatorController = controller, isEnabled = true, type = AnimLayerType.FX, isDefault = false };
Values.AvatarRoot.expressionParameters = expressionParameters;
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
public bool CheckHasParameterSpace(VRCExpressionParameters expressionParameters, int groupCount)
{
int currentCost = expressionParameters.CalcTotalCost();
currentCost -= expressionParameters.parameters.Where(n => string.IsNullOrEmpty(n.name)).Sum(n => VRCExpressionParameters.TypeCost(n.valueType));
int existParamCount = 0;
foreach(var v in Enumerable.Range(1, groupCount))
{
var param = expressionParameters.FindParameter("RadInvStatusG" + v.ToString());
if (param != null)
{
if (param.valueType != VRCExpressionParameters.ValueType.Int)
currentCost -= VRCExpressionParameters.TypeCost(param.valueType);
else
++existParamCount;
}
}
currentCost += (groupCount - existParamCount) * VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int);
if (currentCost <= VRCExpressionParameters.MAX_PARAMETER_COST)
return true;
return false;
}
public AnimatorStateTransition MakeAnyStateTransition(AnimatorStateMachine stateMachine, AnimatorState destState, string paramName, int v, int propsCount, AnimatorConditionMode mode = AnimatorConditionMode.Equals, bool binaryName = true, int binaryOffset = 0)
{
AnimatorStateTransition transition;
transition = stateMachine.AddAnyStateTransition(destState);
transition.hasExitTime = false;
transition.exitTime = 0;
transition.hasFixedDuration = true;
transition.duration = 0;
transition.offset = 0;
if (binaryName)
transition.name = Convert.ToString(v + binaryOffset, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + v.ToString() + "]";
else
transition.name = (v + binaryOffset).ToString() + "(->" + destState.name + ")";
transition.conditions = new AnimatorCondition[] { new AnimatorCondition() { mode = mode, parameter = paramName, threshold = v } };
return transition;
}
public AnimatorStateTransition MakeTransition(AnimatorState srcState, AnimatorState destState, string paramName, int v, int propsCount, AnimatorConditionMode mode = AnimatorConditionMode.Equals, bool binaryName = true, int binaryOffset = 0)
{
AnimatorStateTransition transition;
transition = srcState.AddTransition(destState);
transition.hasExitTime = false;
transition.exitTime = 0;
transition.hasFixedDuration = true;
transition.duration = 0;
transition.offset = 0;
if(binaryName)
transition.name = Convert.ToString(v + binaryOffset, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + v.ToString() + "]";
else
transition.name = (v + binaryOffset).ToString() + "(->" + destState.name + ")";
transition.conditions = new AnimatorCondition[] { new AnimatorCondition() { mode = mode, parameter = paramName, threshold = v } };
return transition;
}
public AnimatorStateTransition MakeTransition(AnimatorState srcState, AnimatorState destState, string paramName, int min, int max, int propsCount)
{
AnimatorStateTransition transition;
transition = srcState.AddTransition(destState);
transition.hasExitTime = false;
transition.exitTime = 0;
transition.hasFixedDuration = true;
transition.duration = 0;
transition.offset = 0;
transition.name = Convert.ToString(min, 2).PadLeft(propsCount, '0') + "-" + Convert.ToString(max, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + min.ToString() + "-" + max.ToString() + "]";
transition.conditions = new AnimatorCondition[]
{
new AnimatorCondition() { mode = AnimatorConditionMode.Greater, parameter = paramName, threshold = min - 1 },
new AnimatorCondition() { mode = AnimatorConditionMode.Less, parameter = paramName, threshold = max + 1 },
};
return transition;
}
public void ClearAllGeneratedStateMachine(AnimatorController controller)
{
var max = controller.layers.Count();
int ofs = 0;
foreach (var layerIndex in Enumerable.Range(0, controller.layers.Count()))
{
UpdateProgressBar("Get all generated state machines.", layerIndex, max);
var name = controller.layers[layerIndex + ofs].name;
if (name.StartsWith("RadInvLayer") || name.StartsWith("RadBitLayer"))
{
controller.RemoveLayer(layerIndex + ofs);
ofs--;
}
}
}
public void UpdateProgressBar(string text, int count, int max)
{
float progress = (float)count / max;
EditorUtility.DisplayProgressBar(text, count.ToString() + "/" + max.ToString() + " (" + (progress * 100) + "%)", progress);
}
public AnimationClip CreateExclusivePropAnim(List<PropInfo> props, PropInfo targetProp, List<Dictionary<EditorCurveBinding, Vector2>> keyframeList, bool generateDefault = false)
{
var clip = new AnimationClip();
foreach (var prop_index in Enumerable.Range(0, props.Count))
{
var prop = props[prop_index];
var keyframes = keyframeList[prop_index];
foreach (var v in keyframes)
{
var curve = new AnimationCurve();
float status;
if (generateDefault)
status = prop.IsDefaultEnabled ? v.Value.y : v.Value.x;
else if (targetProp == prop && (props.Count > 1 || (props.Count <= 1 && !targetProp.IsDefaultEnabled)))
status = v.Value.y;
else
status = v.Value.x;
curve.AddKey(new Keyframe(0f, status));
curve.AddKey(new Keyframe(1f / clip.frameRate, status));
clip.SetCurve(v.Key.path, v.Key.type, v.Key.propertyName, curve);
}
}
return clip;
}
public VRCExpressionsMenu.Control GetOrCreateMenuControl(VRCExpressionsMenu menu, string name, ControlType type)
{
var control = menu.controls.FirstOrDefault(n => n.name == name && n.type == type);
if (control == null)
{
control = new VRCExpressionsMenu.Control();
control.name = name;
control.type = type;
menu.controls.Add(control);
}
return control;
}
public void ReCreateFolder(string path)
{
if (AssetDatabase.IsValidFolder(path))
DeleteAssetDirectory(path);
CreateFolderRecursively(path);
}
public UnityEngine.Object GetOrCreateAsset(string path, Type type)
{
var obj = AssetDatabase.LoadAssetAtPath(path, type);
if (obj == null)
{
obj = CreateInstance(type);
AssetDatabase.CreateAsset(obj, path);
}
return obj;
}
public AnimatorController GetOrCreateAnimator(string generatedItemsPath)
{
AnimatorController controller = null;
if (Values != null && Values.AvatarRoot != null && Values.AvatarRoot.baseAnimationLayers != null &&
Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null)
controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController;
else
controller = AnimatorController.CreateAnimatorControllerAtPath(generatedItemsPath + "FXRadialInventory.controller");
return controller;
}
public void RestoreSettings(SORadialInventory srcValues)
{
Values.Groups = srcValues.Groups.Select(n =>
{
var obj = (GroupInfo)n.Clone();
if (obj != null && obj.Props != null)
foreach (var v in obj.Props)
if (v != null)
{
v.TargetObject = FindGameObjectFromPath(v.TargetObjectPath, Values.AvatarRoot.gameObject);
}
return obj;
}).ToList();
Values.SaveParameter = srcValues.SaveParameter;
Values.UseWriteDefaults = srcValues.UseWriteDefaults;
}
public void SaveSettingsToFile(string generatedItemsPath)
{
var path = generatedItemsPath + "SavedSettings.asset";
SORadialInventory settings = (SORadialInventory)AssetDatabase.LoadAssetAtPath(path, typeof(SORadialInventory));
if (settings == null)
{
settings = ScriptableObject.CreateInstance<SORadialInventory>();
AssetDatabase.CreateAsset(settings, generatedItemsPath + "SavedSettings.asset");
}
foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path))
{
if (AssetDatabase.IsSubAsset(asset))
{
DestroyImmediate(asset, true);
}
}
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
var groupID = 1;
settings.SaveParameter = Values.SaveParameter;
settings.UseWriteDefaults = Values.UseWriteDefaults;
settings.Groups = Values.Groups.Select(n =>
{
var obj = (GroupInfo)n.Clone();
var propID = 1;
obj.Props.ForEach(v =>
{
v.name = "G" + groupID.ToString() + "Prop" + (propID++).ToString();
v.TargetObjectPath = GetGameObjectPath(v.TargetObject, Values.AvatarRoot.gameObject);
AssetDatabase.AddObjectToAsset(v, settings);
});
obj.name = "Group" + (groupID++).ToString();
AssetDatabase.AddObjectToAsset(obj, settings);
return obj;
}).ToList();
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
}
public AnimatorControllerLayer GetOrCreateControllerLayer(AnimatorController controller, string layerName, int weight = 1)
{
AnimatorControllerLayer layer = controller.layers.FirstOrDefault(n => n.name == layerName);
if (layer == null)
{
layer = new AnimatorControllerLayer();
layer.defaultWeight = weight;
layer.blendingMode = AnimatorLayerBlendingMode.Override;
layer.name = layerName;
layer.stateMachine = new AnimatorStateMachine();
controller.AddLayer(layer);
}
if (layer.stateMachine == null)
layer.stateMachine = new AnimatorStateMachine();
return layer;
}
public VRCExpressionParameters CreateExpressionParameters(string path)
{
var param = CreateInstance<VRCExpressionParameters>();
param.parameters = new VRCExpressionParameters.Parameter[16];
param.parameters[0] = new VRCExpressionParameters.Parameter() { name = "VRCEmote", valueType = VRCExpressionParameters.ValueType.Int };
param.parameters[1] = new VRCExpressionParameters.Parameter() { name = "VRCFaceBlendH", valueType = VRCExpressionParameters.ValueType.Float };
param.parameters[2] = new VRCExpressionParameters.Parameter() { name = "VRCFaceBlendV", valueType = VRCExpressionParameters.ValueType.Float };
AssetDatabase.CreateAsset(param, path);
return param;
}
public bool CheckParameter(VRCExpressionParameters expressionParameters, string paramName, AnimatorController controller, bool saved)
{
int i = -1;
var cost = expressionParameters.CalcTotalCost();
var blankParams = expressionParameters.parameters.Where(n => string.IsNullOrEmpty(n.name));
cost -= blankParams.Sum(n => VRCExpressionParameters.TypeCost(n.valueType));
var param = expressionParameters.FindParameter(paramName);
if(param == null)
{
if(blankParams.Any())
{
foreach(var v in Enumerable.Range(0, expressionParameters.parameters.Count()))
{
if(string.IsNullOrEmpty(expressionParameters.parameters[v].name))
{
param = expressionParameters.parameters[v];
var newCost = cost - VRCExpressionParameters.TypeCost(param.valueType) + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int);
if (newCost <= VRCExpressionParameters.MAX_PARAMETER_COST)
{
param.valueType = VRCExpressionParameters.ValueType.Int;
param.name = paramName;
break;
}
else
return false;
}
}
}
else
{
if (cost + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int) <= VRCExpressionParameters.MAX_PARAMETER_COST)
{
var len = expressionParameters.parameters.Length;
Array.Resize(ref expressionParameters.parameters, len + 1);
param = new VRCExpressionParameters.Parameter();
expressionParameters.parameters[len] = param;
expressionParameters.parameters[len].name = paramName;
expressionParameters.parameters[len].valueType = VRCExpressionParameters.ValueType.Int;
}
else
return false;
}
}
else if(param.valueType != VRCExpressionParameters.ValueType.Int)
{
var newCost = cost - VRCExpressionParameters.TypeCost(param.valueType) + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int);
if (newCost <= VRCExpressionParameters.MAX_PARAMETER_COST)
{
param.valueType = VRCExpressionParameters.ValueType.Int;
}
else
return false;
}
param.saved = saved;
if (!controller.parameters.Any(n => n.name == paramName))
controller.AddParameter(paramName, AnimatorControllerParameterType.Int);
else
{
var first = controller.parameters.First(n => n.name == paramName);
if (first.type != AnimatorControllerParameterType.Int)
first.type = AnimatorControllerParameterType.Int;
}
return true;
}
public void CreateFolderRecursively(string path)
{
Debug.Assert(path.StartsWith("Assets/"), "arg `path` of CreateFolderRecursively doesn't starts with `Assets/`");
if (AssetDatabase.IsValidFolder(path)) return;
if (path[path.Length - 1] == '/')
{
path = path.Substring(0, path.Length - 1);
}
var names = path.Split('/');
for (int i = 1; i < names.Length; i++)
{
var parent = string.Join("/", names.Take(i).ToArray());
var target = string.Join("/", names.Take(i + 1).ToArray());
var child = names[i];
if (!AssetDatabase.IsValidFolder(target))
{
AssetDatabase.CreateFolder(parent, child);
}
}
}
public string GetGameObjectPath(GameObject obj, GameObject parent = null, int recCount = -1)
{
string path = "/" + obj.name;
int i = 0;
string parentpath = "";
if (parent != null)
parentpath = GetGameObjectPath(parent);
while (obj.transform.parent != null && (recCount == -1 || i < recCount))
{
++i;
obj = obj.transform.parent.gameObject;
path = "/" + obj.name + path;
}
Debug.Log("[old]path: " + path);
var parentpathLength = parentpath.Length;
if (!string.IsNullOrEmpty(parentpath))
{
path = path.Remove(1,parentpathLength);
}
Debug.Log("[new]path: " + path);
return path.TrimStart('/');
}
private bool DeleteAssetDirectory(string assetPath)
{
if (AssetDatabase.DeleteAsset(assetPath))
{
Debug.Log("Delete Asset: " + assetPath);
return true;
}
string[] dirpathlist = Directory.GetDirectories(assetPath);
foreach (string path in dirpathlist)
{
if (false == DeleteAssetDirectory(path))
{
Debug.LogError("Delete Asset Directory Error: " + path);
return false;
}
}
string[] filepathlist = Directory.GetFiles(assetPath);
foreach (string path in filepathlist)
{
if (path.EndsWith(".meta"))
{
continue;
}
if (false == AssetDatabase.DeleteAsset(path))
{
Debug.LogError("Delete Asset Files Error: " + path);
return false;
}
}
Debug.Log("Delete Asset: " + assetPath);
return true;
}
public GameObject FindGameObjectFromPath(string path, GameObject root = null)
{
var splittedPath = path.Split('/');
GameObject currentObject = root;
foreach (var v in splittedPath)
{
if (currentObject == null)
currentObject = GameObject.Find(v);
else
currentObject = currentObject.transform.Find(v)?.gameObject;
if (currentObject == null)
{
Debug.Log("[Warn] currentObject == null");
return null;
}
}
return currentObject;
}
}
@aki-lua87
Copy link
Author

aki-lua87 commented Apr 9, 2021

メリノさんでなんか変になるやつの修正
2.9向け

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment