Skip to content

Commit

Permalink
feat: implement customizable and related vrc integrations (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
poi-vrc authored Aug 28, 2023
1 parent fc2d61c commit f9ed511
Show file tree
Hide file tree
Showing 12 changed files with 424 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ private void GenerateWearableBlendshapeAnimations(AnimationClip enableClip, Anim
var obj = _wearableObject.transform.Find(blendshape.path);
if (obj == null)
{
DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredAvatarBlendshapeObjectNotFound, _wearableObject.name, blendshape.path);
DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredWearableBlendshapeObjectNotFound, _wearableObject.name, blendshape.path);
continue;
}

if (!TryGetBlendshapeValue(obj.gameObject, blendshape.blendshapeName, out var originalValue))
Expand Down Expand Up @@ -235,7 +236,7 @@ public System.Tuple<AnimationClip, AnimationClip> GenerateWearAnimations()
return new System.Tuple<AnimationClip, AnimationClip>(enableClip, disableClip);
}

public Dictionary<WearableCustomizable, System.Tuple<AnimationClip, AnimationClip>> GenerateCustomizableAnimations()
public Dictionary<WearableCustomizable, System.Tuple<AnimationClip, AnimationClip>> GenerateCustomizableToggleAnimations()
{
// prevent unexpected behaviour
if (!DTEditorUtils.IsGrandParent(_avatarObject.transform, _wearableObject.transform))
Expand All @@ -250,14 +251,14 @@ public System.Tuple<AnimationClip, AnimationClip> GenerateWearAnimations()
var enableClip = new AnimationClip();
var disableClip = new AnimationClip();

if (customizable.type == WearableCustomizableType.Toggle)
{
// avatar required toggles
GenerateAvatarToggleAnimations(enableClip, disableClip, customizable.avatarRequiredToggles.ToArray());
// avatar required toggles
GenerateAvatarToggleAnimations(enableClip, disableClip, customizable.avatarToggles.ToArray());

// avatar required blendshapes
GenerateAvatarBlendshapeAnimations(enableClip, disableClip, customizable.avatarRequiredBlendshapes.ToArray());
// avatar required blendshapes
GenerateAvatarBlendshapeAnimations(enableClip, disableClip, customizable.avatarBlendshapes.ToArray());

if (customizable.type == WearableCustomizableType.Toggle)
{
// wearable required blendshapes
GenerateWearableBlendshapeAnimations(enableClip, disableClip, customizable.wearableBlendshapes.ToArray());

Expand All @@ -266,14 +267,51 @@ public System.Tuple<AnimationClip, AnimationClip> GenerateWearAnimations()
}
else if (customizable.type == WearableCustomizableType.Blendshape)
{
// TODO: we need to create a curve from 0.0f to 100.0f to handle this type of customizable
throw new System.NotImplementedException();
// wearable required toggle
GenerateWearableToggleAnimations(enableClip, disableClip, customizable.wearableToggles.ToArray());

// we only the toggles here, for radial blendshapes, we need to separate a layer to do that
}

dict.Add(customizable, new System.Tuple<AnimationClip, AnimationClip>(enableClip, disableClip));
}

return dict;
}

public Dictionary<WearableCustomizable, AnimationClip> GenerateCustomizableBlendshapeAnimations()
{
// prevent unexpected behaviour
if (!DTEditorUtils.IsGrandParent(_avatarObject.transform, _wearableObject.transform))
{
throw new System.Exception("Wearable object is not inside avatar! Cannot proceed animation generation.");
}

var dict = new Dictionary<WearableCustomizable, AnimationClip>();

foreach (var customizable in _module.wearableCustomizables)
{
if (customizable.type == WearableCustomizableType.Blendshape)
{
var clip = new AnimationClip();

foreach (var wearableBlendshape in customizable.wearableBlendshapes)
{
var obj = _wearableObject.transform.Find(wearableBlendshape.path);
if (obj == null)
{
DTReportUtils.LogWarnLocalized(_report, LogLabel, MessageCode.IgnoredWearableBlendshapeObjectNotFound, _wearableObject.name, wearableBlendshape.path);
continue;
}

AnimationUtils.SetLinearZeroToHundredBlendshapeCurve(clip, AnimationUtils.GetRelativePath(obj.transform, _avatarObject.transform), wearableBlendshape.blendshapeName);
}

dict.Add(customizable, clip);
}
}

return dict;
}
}
}
14 changes: 13 additions & 1 deletion Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

using System.Collections.Generic;
using System.IO;
using System.Linq;
using Chocopoi.DressingTools.Lib.Cabinet;
using Chocopoi.DressingTools.Lib.Cabinet.Modules;
using Chocopoi.DressingTools.Lib.Extensibility.Providers;
Expand All @@ -36,6 +37,8 @@ internal class DTEditorUtils

private static Dictionary<string, System.Type> s_reflectionTypeCache = new Dictionary<string, System.Type>();

private static readonly System.Random Random = new System.Random();

private static List<List<string>> s_boneNameMappings = null;

//Reference: https://forum.unity.com/threads/horizontal-line-in-editor-window.520812/#post-3416790
Expand Down Expand Up @@ -77,7 +80,7 @@ public static DTCabinet GetAvatarCabinet(GameObject avatar, bool createIfNotExis
// TODO: read default config, scan for armature names?
comp.avatarGameObject = avatar;
var config = new CabinetConfig();
comp.configJson = config.ToString();
comp.configJson = config.Serialize();
}

return comp;
Expand Down Expand Up @@ -637,5 +640,14 @@ public static bool IsOriginatedFromAnyWearable(Transform root, Transform transfo
}
return found;
}

public static string RandomString(int length)
{
// i just copied from stackoverflow :D
// https://stackoverflow.com/questions/1344221/how-can-i-generate-random-alphanumeric-strings?page=1&tab=scoredesc#tab-top
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[Random.Next(s.Length)]).ToArray());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,8 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx)

ExpressionMenuUtils.RemoveExpressionParameters(exParams, "^cpDT_Cabinet");

try
var parametersToAdd = new List<VRCExpressionParameters.Parameter>
{
ExpressionMenuUtils.AddExpressionParameters(exParams, new VRCExpressionParameters.Parameter[]
{
new VRCExpressionParameters.Parameter()
{
name = "cpDT_Cabinet",
Expand All @@ -142,20 +140,13 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx)
networkSynced = true,
saved = true
}
});
}
catch (ParameterOverflowException ex)
{
DTReportUtils.LogExceptionLocalized(cabCtx.report, LogLabel, ex, "integrations.vrc.msgCode.error.parameterOverFlow");
return false;
}
};

ExpressionMenuUtils.RemoveExpressionMenuControls(exMenu, "DT Cabinet");

var subMenu = new ExpressionMenuBuilder(exMenu)
.BeginNewSubMenu("DT Cabinet");

subMenu.AddToggle("Original", "cpDT_Cabinet", 0);
var baseSubMenu = new ExpressionMenuBuilder();
baseSubMenu.AddToggle("Original", "cpDT_Cabinet", 0);
baseSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet.asset", CabinetApplier.GeneratedAssetsPath));

// create an empty clip
var emptyClip = new AnimationClip();
Expand All @@ -167,6 +158,9 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx)

// get wearables
var wearables = DTEditorUtils.GetCabinetWearables(cabCtx.avatarGameObject);
var cabinetMenu = baseSubMenu;
var cabinetMenuIndex = 0;
var originalLayerLength = fxController.layers.Length;

for (var i = 0; i < wearables.Length; i++)
{
Expand Down Expand Up @@ -198,21 +192,122 @@ public static bool ApplyAnimationsAndMenu(ApplyCabinetContext cabCtx)
pairs.Add(i + 1, wearAnimations.Item1); // enable clip
AssetDatabase.CreateAsset(wearAnimations.Item1, CabinetApplier.GeneratedAssetsPath + "/cpDT_" + wearables[i].name + ".anim");

// generate expression menu
subMenu.AddToggle(vrcm.customCabinetToggleName ?? config.info.name, "cpDT_Cabinet", i + 1);
// add a new submenu if going to be full
if (cabinetMenu.GetMenu().controls.Count == 7)
{
var newSubMenu = new ExpressionMenuBuilder();
newSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}.asset", CabinetApplier.GeneratedAssetsPath, ++cabinetMenuIndex));

cabinetMenu.AddSubMenu("Next Page", newSubMenu.GetMenu());
cabinetMenu = newSubMenu;
}

if (agm.wearableCustomizables.Count > 0)
{
// Add a submenu for wearables that have customizables
var baseCustomizableSubMenu = new ExpressionMenuBuilder();
baseCustomizableSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}_{2}.asset", CabinetApplier.GeneratedAssetsPath, cabinetMenuIndex, config.info.name));
cabinetMenu.AddSubMenu(vrcm.customCabinetToggleName ?? config.info.name, baseCustomizableSubMenu.GetMenu());

// add enable toggle
baseCustomizableSubMenu.AddToggle("Enable", "cpDT_Cabinet", i + 1);

var customizableMenu = baseCustomizableSubMenu;
var customizableMenuIndex = 0;

var customizableToggleAnimations = animationGenerator.GenerateCustomizableToggleAnimations();
var customizableBlendshapeAnimations = animationGenerator.GenerateCustomizableBlendshapeAnimations();

foreach (var wearableCustomizable in agm.wearableCustomizables)
{
// add a new submenu if going to be full
if (customizableMenu.GetMenu().controls.Count == 7)
{
var newSubMenu = new ExpressionMenuBuilder();
newSubMenu.CreateAsset(string.Format("{0}/cpDT_Cabinet_{1}_{2}_{3}.asset", CabinetApplier.GeneratedAssetsPath, cabinetMenuIndex, config.info.name, ++customizableMenuIndex));

cabinetMenu.AddSubMenu("Next Page", newSubMenu.GetMenu());
customizableMenu = newSubMenu;
}

var parameterName = string.Format("cpDT_Cabinet_{0}_{1}", wearableCustomizable.name, DTEditorUtils.RandomString(8));

if (wearableCustomizable.type == WearableCustomizableType.Toggle)
{
AnimationUtils.AddAnimatorParameter(fxController, parameterName, wearableCustomizable.defaultValue > 0);
parametersToAdd.Add(new VRCExpressionParameters.Parameter()
{
name = wearableCustomizable.name,
valueType = VRCExpressionParameters.ValueType.Bool,
defaultValue = wearableCustomizable.defaultValue,
networkSynced = true,
saved = true
});

var anims = customizableToggleAnimations[wearableCustomizable];
AssetDatabase.CreateAsset(anims.Item1, string.Format("{0}/{1}_On.anim", CabinetApplier.GeneratedAssetsPath, parameterName));
AssetDatabase.CreateAsset(anims.Item2, string.Format("{0}/{1}_Off.anim", CabinetApplier.GeneratedAssetsPath, parameterName));
AnimationUtils.GenerateSingleToggleLayer(fxController, parameterName, parameterName, anims.Item2, anims.Item1, cabCtx.cabinetConfig.animationWriteDefaults, false, null, refTransition);
customizableMenu.AddToggle(wearableCustomizable.name, parameterName, 1);
}
else if (wearableCustomizable.type == WearableCustomizableType.Blendshape)
{
AnimationUtils.AddAnimatorParameter(fxController, parameterName, wearableCustomizable.defaultValue);
parametersToAdd.Add(new VRCExpressionParameters.Parameter()
{
name = wearableCustomizable.name,
valueType = VRCExpressionParameters.ValueType.Float,
defaultValue = wearableCustomizable.defaultValue,
networkSynced = true,
saved = true
});

var toggleAnims = customizableToggleAnimations[wearableCustomizable];
AssetDatabase.CreateAsset(toggleAnims.Item1, string.Format("{0}/{1}_On.anim", CabinetApplier.GeneratedAssetsPath, parameterName));
AssetDatabase.CreateAsset(toggleAnims.Item2, string.Format("{0}/{1}_Off.anim", CabinetApplier.GeneratedAssetsPath, parameterName));
var blendshapeAnim = customizableBlendshapeAnimations[wearableCustomizable];
AssetDatabase.CreateAsset(blendshapeAnim, string.Format("{0}/{1}_MotionTime.anim", CabinetApplier.GeneratedAssetsPath, parameterName));
AnimationUtils.GenerateSingleToggleLayer(fxController, parameterName + "_Toggles", parameterName, toggleAnims.Item2, toggleAnims.Item1, cabCtx.cabinetConfig.animationWriteDefaults, false, null, refTransition);
AnimationUtils.GenerateSingleMotionTimeLayer(fxController, parameterName + "_Blendshapes", parameterName, blendshapeAnim, cabCtx.cabinetConfig.animationWriteDefaults);
customizableMenu.AddRadialPuppet(wearableCustomizable.name, parameterName);
}
}
}
else
{
// Add toggle only
cabinetMenu.AddToggle(vrcm.customCabinetToggleName ?? config.info.name, "cpDT_Cabinet", i + 1);
}
}

// create cabinet layer
AnimationUtils.GenerateAnyStateLayer(fxController, "cpDT_Cabinet", "cpDT_Cabinet", pairs, cabCtx.cabinetConfig.animationWriteDefaults, null, refTransition);

EditorUtility.DisplayProgressBar("DressingTools", "Generating expression menu...", 0);
subMenu.CreateAsset(CabinetApplier.GeneratedAssetsPath + "/cpDT_Cabinet.asset")
.EndNewSubMenu();
// we push our cabinet layer to the top
var layerList = new List<AnimatorControllerLayer>(fxController.layers);
var cabinetLayer = layerList[layerList.Count - 1];
layerList.Remove(cabinetLayer);
layerList.Insert(originalLayerLength, cabinetLayer);
fxController.layers = layerList.ToArray();

EditorUtility.DisplayProgressBar("DressingTools", "Adding expression parameters...", 0);

try
{
ExpressionMenuUtils.AddExpressionParameters(exParams, parametersToAdd);
ExpressionMenuUtils.AddSubMenu(exMenu, "DT Cabinet", baseSubMenu.GetMenu());
}
catch (ParameterOverflowException ex)
{
DTReportUtils.LogExceptionLocalized(cabCtx.report, LogLabel, ex, "integrations.vrc.msgCode.error.parameterOverFlow");
return false;
}

EditorUtility.SetDirty(fxController);
EditorUtility.SetDirty(exMenu);
EditorUtility.SetDirty(exParams);

EditorUtility.ClearProgressBar();
EditorUtility.DisplayProgressBar("DressingTools", "Saving assets...", 0);
AssetDatabase.SaveAssets();

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private void OnCabinetSettingsChange()
_cabinetConfig = new CabinetConfig();
}
_cabinetConfig.avatarArmatureName = _view.CabinetAvatarArmatureName;
cabinet.configJson = _cabinetConfig.ToString();
cabinet.configJson = _cabinetConfig.Serialize();
}

private void OnForceUpdateView()
Expand Down
Loading

0 comments on commit f9ed511

Please sign in to comment.