summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Examples/ex0_animator.json5
-rw-r--r--YOTSCore.cs (renamed from Editor/Generate_Animator.cs)664
-rw-r--r--YOTSNDMFConfig.cs11
-rw-r--r--YOTSNDMFGenerator.cs91
4 files changed, 286 insertions, 485 deletions
diff --git a/Examples/ex0_animator.json b/Examples/ex0_animator.json
index cf8a91c..51a262c 100644
--- a/Examples/ex0_animator.json
+++ b/Examples/ex0_animator.json
@@ -3,7 +3,6 @@
"toggles": [
{
"name": "Shirt",
- "menuPath": "/Clothes",
"meshToggles": ["Shirt"],
"blendShapes": [
{
@@ -21,10 +20,6 @@
"path": "Body",
"blendShape": "Chest_Hide"
},
- {
- "path": "Shirt",
- "blendShape": "Chest_Hide"
- }
]
}
]
diff --git a/Editor/Generate_Animator.cs b/YOTSCore.cs
index 367bba7..e30042d 100644
--- a/Editor/Generate_Animator.cs
+++ b/YOTSCore.cs
@@ -1,9 +1,10 @@
+using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
-using System.Linq;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
@@ -167,163 +168,85 @@ namespace YOTS
public Texture2D icon;
}
- // This class adds both a menu command and GUI window for animator generation
- public class GenerateAnimatorCommand : EditorWindow
+ public class YOTSCore
{
- private string jsonPath;
- private string animatorName = "YOTS_FX";
- private string existingParamsPath;
- private string existingMenuPath;
- private VRCExpressionParameters existingParams;
- private VRCExpressionsMenu existingMenu;
-
- [MenuItem("Tools/yum_food/YOTS")]
- public static void ShowWindow()
+ public static AnimatorController GenerateAnimator(
+ string configJson,
+ VRCExpressionParameters existingParams,
+ VRCExpressionsMenu mainMenu,
+ Action<UnityEngine.Object> saveAsset
+ )
{
- GetWindow<GenerateAnimatorCommand>("YOTS");
- }
+ Debug.Log("=== Starting Animator Generation Process ===");
- private void OnGUI()
- {
- GUILayout.Label("YOTS Animator Generator", EditorStyles.boldLabel);
-
- // Create a drag-drop field for the JSON config
- var jsonObj = AssetDatabase.LoadAssetAtPath<TextAsset>(jsonPath);
- var newJsonObj = (TextAsset)EditorGUILayout.ObjectField(
- "Config JSON",
- jsonObj,
- typeof(TextAsset),
- false
- );
- if (newJsonObj != jsonObj)
+ if (string.IsNullOrEmpty(configJson))
{
- jsonPath = AssetDatabase.GetAssetPath(newJsonObj);
- }
- if (string.IsNullOrEmpty(jsonPath))
- {
- EditorGUILayout.HelpBox("Config JSON must be provided.", MessageType.Error);
+ Debug.LogError("No JSON content provided. Process aborted.");
+ return null;
}
+ Debug.Log("Parsing JSON configuration");
- animatorName = EditorGUILayout.TextField("Animator Name", animatorName);
-
- // Replace file path fields with Object fields for drag-and-drop
- existingParams = (VRCExpressionParameters)EditorGUILayout.ObjectField(
- "VRC Parameters",
- existingParams,
- typeof(VRCExpressionParameters),
- false
- );
- existingParamsPath = existingParams != null ? AssetDatabase.GetAssetPath(existingParams) : null;
-
- existingMenu = (VRCExpressionsMenu)EditorGUILayout.ObjectField(
- "VRC Menu",
- existingMenu,
- typeof(VRCExpressionsMenu),
- false
- );
- existingMenuPath = existingMenu != null ? AssetDatabase.GetAssetPath(existingMenu) : null;
-
- // Show error message if either field is missing
- if (existingParams == null || existingMenu == null)
+ AnimatorConfigFile config;
+ try
{
- EditorGUILayout.HelpBox("VRC parameters and menu must be provided.", MessageType.Error);
+ config = JsonUtility.FromJson<AnimatorConfigFile>(configJson);
}
-
- GUI.enabled = !string.IsNullOrEmpty(jsonPath) && existingParams != null && existingMenu != null;
- if (GUILayout.Button("Generate Animator"))
+ catch (System.Exception e)
{
- if (string.IsNullOrEmpty(jsonPath))
- {
- EditorUtility.DisplayDialog("Error", "Please select a configuration file.", "OK");
- return;
- }
- GenerateAnimator(jsonPath, animatorName, existingParamsPath, existingMenuPath);
+ Debug.LogError($"JSON parsing failed: {e.Message}");
+ return null;
}
- GUI.enabled = true;
- }
-
- private static AnimationClip AssignOrCreateAnimationClip(AnimationClip newClip, string clipPath)
- {
- AnimationClip existingClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
- if (existingClip != null)
+ if (config == null)
{
- // Clear existing curves
- var existingBindings = AnimationUtility.GetCurveBindings(existingClip);
- foreach (var binding in existingBindings)
- {
- AnimationUtility.SetEditorCurve(existingClip, binding, null);
- }
- // Copy new curves from our temporary clip
- var newBindings = AnimationUtility.GetCurveBindings(newClip);
- foreach (var binding in newBindings)
- {
- var curve = AnimationUtility.GetEditorCurve(newClip, binding);
- AnimationUtility.SetEditorCurve(existingClip, binding, curve);
- }
- EditorUtility.SetDirty(existingClip);
- return existingClip;
+ Debug.LogError("Configuration file is empty or invalid");
+ return null;
}
- else
+
+ if (config.toggles == null)
{
- AssetDatabase.CreateAsset(newClip, clipPath);
- return newClip;
+ Debug.LogError("No toggleSpecs found in configuration");
+ return null;
}
+ Debug.Log($"Configuration loaded. Found {config.toggles.Count} toggles.");
+
+ // First we generate a naive animator config.
+ GeneratedAnimatorConfig genAnimatorConfig = GenerateNaiveAnimatorConfig(config.toggles);
+ // Apply further fixes
+ genAnimatorConfig = ApplyIndependentFixToAnimatorConfig(genAnimatorConfig);
+ genAnimatorConfig = RemoveOffAnimationsFromOverrideLayers(genAnimatorConfig);
+ genAnimatorConfig = RemoveUnusedAnimations(genAnimatorConfig);
+
+ // Generate VRChat parameters and menu
+ GenerateVRChatAssets(config.toggles, saveAsset, existingParams, mainMenu);
+
+ // Create the animation clips directly from the animator config
+ // TODO animations should not be persisted to disk unless requested for debuggability
+ CreateAnimationClips(new GeneratedAnimationsConfig { animations = genAnimatorConfig.animations });
+
+ // Generate and return the animator controller
+ AnimatorController controller = GenerateAnimatorController(genAnimatorConfig, saveAsset);
+
+ Debug.Log("=== Animator Generation Process Complete ===");
+ return controller;
}
- private static GeneratedAnimationsConfig GenerateAnimationConfig(List<ToggleSpec> toggleSpecs)
+ private static AnimationClip AssignOrCreateAnimationClip(AnimationClip newClip, Action<UnityEngine.Object> saveAsset)
{
- GeneratedAnimationsConfig genAnimConfig = new GeneratedAnimationsConfig();
- foreach (var toggle in toggleSpecs)
- {
- GeneratedAnimationClipConfig onAnim = new GeneratedAnimationClipConfig();
- onAnim.name = toggle.name + "_On";
- if (toggle.meshToggles != null)
- {
- foreach (var mesh in toggle.meshToggles)
- {
- onAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 1.0f });
- }
- }
- if (toggle.blendShapes != null)
- {
- foreach (var bs in toggle.blendShapes)
- {
- onAnim.blendShapes.Add(new GeneratedBlendShape {
- path = bs.path,
- blendShape = bs.blendShape,
- value = bs.onValue
- });
- }
- }
- genAnimConfig.animations.Add(onAnim);
-
- GeneratedAnimationClipConfig offAnim = new GeneratedAnimationClipConfig();
- offAnim.name = toggle.name + "_Off";
- if (toggle.meshToggles != null)
- {
- foreach (var mesh in toggle.meshToggles)
- {
- offAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 0.0f });
- }
- }
- if (toggle.blendShapes != null)
- {
- foreach (var bs in toggle.blendShapes)
- {
- offAnim.blendShapes.Add(new GeneratedBlendShape {
- path = bs.path,
- blendShape = bs.blendShape,
- value = bs.offValue
- });
- }
- }
- genAnimConfig.animations.Add(offAnim);
- }
- return genAnimConfig;
+ saveAsset(newClip);
+ return newClip;
}
- private static void CreateAnimationClips(GeneratedAnimationsConfig animationsConfig, string outputDir)
+ private static void CreateAnimationClips(GeneratedAnimationsConfig animationsConfig)
{
+ // Ensure the directory exists
+ string baseDir = "Assets/YOTS_generated";
+ string animDir = Path.Combine(baseDir, "Animations");
+
+ if (!Directory.Exists(baseDir))
+ Directory.CreateDirectory(baseDir);
+ if (!Directory.Exists(animDir))
+ Directory.CreateDirectory(animDir);
+
foreach (var clipConfig in animationsConfig.animations)
{
AnimationClip newClip = new AnimationClip();
@@ -351,60 +274,34 @@ namespace YOTS
AnimationUtility.SetEditorCurve(newClip, binding, curve);
}
- string clipPath = Path.Combine(outputDir, "Animations", $"{clipConfig.name}.anim");
- AnimationClip clip = AssignOrCreateAnimationClip(newClip, clipPath);
- Debug.Log("Created/Updated animation clip: " + clipConfig.name + " at path: " + clipPath);
+ // Save the animation clip to the specified directory
+ string assetPath = Path.Combine(animDir, clipConfig.name + ".anim");
+ AssetDatabase.CreateAsset(newClip, assetPath);
+ Debug.Log("Created/Updated animation clip: " + assetPath);
}
+
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
}
- private static AnimatorController InitializeAnimatorController(string controllerPath)
+ private static AnimatorController InitializeAnimatorController(Action<UnityEngine.Object> saveAsset)
{
- AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
- if (controller == null)
- {
- controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
- Debug.Log("Created new AnimatorController at: " + controllerPath);
- }
- else
- {
- Debug.Log("Reusing existing AnimatorController GUID at: " + controllerPath);
-
- // Clean up all sub-assets (BlendTrees, StateMachines) before clearing parameters and layers
- var subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(controllerPath);
- foreach (var subAsset in subAssets)
- {
- if (subAsset is BlendTree || subAsset is AnimatorStateMachine)
- {
- Object.DestroyImmediate(subAsset, true);
- }
- }
-
- // Clear parameters and layers
- while (controller.parameters.Length > 0)
- {
- controller.RemoveParameter(controller.parameters[0]);
- }
- while (controller.layers.Length > 0)
- {
- controller.RemoveLayer(0);
- }
- }
+ AnimatorController controller = new AnimatorController();
+ saveAsset(controller);
+ Debug.Log("Created new AnimatorController");
return controller;
}
- private static void GenerateAnimatorController(GeneratedAnimatorConfig animatorConfig, string generatedOutputDir)
+ private static AnimatorController GenerateAnimatorController(GeneratedAnimatorConfig animatorConfig, Action<UnityEngine.Object> saveAsset)
{
- string controllerPath = Path.Combine(generatedOutputDir, $"{animatorConfig.name}.controller");
- AnimatorController controller = InitializeAnimatorController(controllerPath);
+ AnimatorController controller = InitializeAnimatorController(saveAsset);
+
+ // Save controller first to ensure it exists
+ saveAsset(controller);
- // This is always set to 1 and used to ensure that each DBT always
- // animates.
- // More info on vrc.schooL:
- // https://vrc.school/docs/Other/DBT-Combining
controller.AddParameter("YOTS_Weight", AnimatorControllerParameterType.Float);
controller.parameters[0].defaultFloat = 1.0f;
- // Add parameters from the config as float parameters
foreach (var param in animatorConfig.parameters)
{
if (!controller.parameters.Any(p => p.name == param))
@@ -417,15 +314,13 @@ namespace YOTS
var baseLayer = animatorConfig.layers[0];
var baseStateMachine = new AnimatorStateMachine();
baseStateMachine.name = "BaseLayer_SM";
- AssetDatabase.AddObjectToAsset(baseStateMachine, controller);
+ saveAsset(baseStateMachine);
- // Create the root Direct Blend Tree
var rootBlendTree = new BlendTree();
rootBlendTree.name = "BaseLayer_RootBlendTree";
rootBlendTree.blendType = BlendTreeType.Direct;
- AssetDatabase.AddObjectToAsset(rootBlendTree, controller);
+ saveAsset(rootBlendTree);
- // Create 1D blend trees for each parameter in the base layer
var parameterGroups = baseLayer.directBlendTree.entries
.GroupBy(e => e.parameter)
.ToDictionary(g => g.Key, g => g.ToList());
@@ -435,19 +330,17 @@ namespace YOTS
var param = group.Key;
var entries = group.Value;
- // Create 1D blend tree for this parameter
var paramBlendTree = new BlendTree();
paramBlendTree.name = $"BlendTree_{param}";
paramBlendTree.blendType = BlendTreeType.Simple1D;
paramBlendTree.blendParameter = param;
- AssetDatabase.AddObjectToAsset(paramBlendTree, controller);
+ saveAsset(paramBlendTree);
- // Add On/Off animations to the 1D blend tree
var children = new List<ChildMotion>();
foreach (var entry in entries.OrderBy(e => e.name.EndsWith("_On")))
{
Debug.Log("Adding child motion for: " + entry.name);
- string clipPath = Path.Combine(generatedOutputDir, "Animations", $"{entry.name}.anim");
+ string clipPath = $"Assets/YOTS_generated/Animations/{entry.name}.anim";
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
if (clip == null)
{
@@ -464,7 +357,6 @@ namespace YOTS
}
paramBlendTree.children = children.ToArray();
- // Add this 1D blend tree to the root Direct Blend Tree
rootBlendTree.children = rootBlendTree.children.Append(new ChildMotion
{
motion = paramBlendTree,
@@ -473,13 +365,11 @@ namespace YOTS
}).ToArray();
}
- // Set up base layer state
var baseState = baseStateMachine.AddState("BaseLayer_State");
baseState.motion = rootBlendTree;
baseState.writeDefaultValues = true;
baseStateMachine.defaultState = baseState;
- // Add base layer to controller
controller.AddLayer(new AnimatorControllerLayer
{
name = "YOTS_BaseLayer",
@@ -495,7 +385,7 @@ namespace YOTS
var stateMachine = new AnimatorStateMachine();
stateMachine.name = layerName + "_SM";
- AssetDatabase.AddObjectToAsset(stateMachine, controller);
+ saveAsset(stateMachine);
var blendTree = new BlendTree();
blendTree.name = layerName + "_BlendTree";
@@ -503,7 +393,7 @@ namespace YOTS
foreach (var entry in layerConfig.directBlendTree.entries)
{
- string clipPath = Path.Combine(generatedOutputDir, "Animations", $"{entry.name}.anim");
+ string clipPath = $"Assets/YOTS_generated/Animations/{entry.name}.anim";
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
if (clip == null)
{
@@ -518,7 +408,7 @@ namespace YOTS
directBlendParameter = entry.parameter
}).ToArray();
}
- AssetDatabase.AddObjectToAsset(blendTree, controller);
+ saveAsset(blendTree);
var state = stateMachine.AddState(layerName + "_State");
state.motion = blendTree;
@@ -535,43 +425,33 @@ namespace YOTS
Debug.Log($"Added override layer: {layerName}");
}
- EditorUtility.SetDirty(controller);
- AssetDatabase.SaveAssets();
- Debug.Log("Animator Controller generation complete at: " + controllerPath);
+ return controller;
}
private static Dictionary<string, int> TopologicalSortToggles(List<ToggleSpec> toggleSpecs)
{
- // Create adjacency list
Dictionary<string, HashSet<string>> graph = new Dictionary<string, HashSet<string>>();
foreach (var toggle in toggleSpecs)
{
if (!graph.ContainsKey(toggle.name))
- {
graph[toggle.name] = new HashSet<string>();
- }
foreach (var dep in toggle.dependencies)
{
if (!graph.ContainsKey(dep))
- {
graph[dep] = new HashSet<string>();
- }
graph[dep].Add(toggle.name);
}
}
- // Calculate in-degrees
Dictionary<string, int> inDegree = new Dictionary<string, int>();
foreach (var toggle in toggleSpecs)
{
inDegree[toggle.name] = toggle.dependencies.Count;
}
- // Perform topological sort with depth tracking
Dictionary<string, int> depths = new Dictionary<string, int>();
Queue<string> queue = new Queue<string>();
- // Add all nodes with no dependencies to queue with depth 0
foreach (var pair in inDegree)
{
if (pair.Value == 0)
@@ -587,7 +467,6 @@ namespace YOTS
string current = queue.Dequeue();
processedNodes++;
int currentDepth = depths[current];
-
if (graph.ContainsKey(current))
{
foreach (var neighbor in graph[current])
@@ -602,10 +481,8 @@ namespace YOTS
}
}
- // Check for cycles
if (processedNodes != toggleSpecs.Count)
{
- // Find nodes involved in the cycle for a better error message
var cycleNodes = toggleSpecs
.Where(t => !depths.ContainsKey(t.name))
.Select(t => t.name)
@@ -617,24 +494,20 @@ namespace YOTS
return depths;
}
- private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig(List<ToggleSpec> toggleSpecs, string animatorName)
+ private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig(List<ToggleSpec> toggleSpecs)
{
GeneratedAnimatorConfig genAnimatorConfig = new GeneratedAnimatorConfig();
- genAnimatorConfig.name = animatorName;
+ genAnimatorConfig.name = "YOTS_Animator";
- // Generate animations
GeneratedAnimationsConfig animConfig = GenerateAnimationConfig(toggleSpecs);
- genAnimatorConfig.animations = animConfig.animations; // Store animations inline
+ genAnimatorConfig.animations = animConfig.animations;
- // Topologically sort the toggles according to their dependencies
Dictionary<string, int> depths = TopologicalSortToggles(toggleSpecs);
-
var togglesByDepth = toggleSpecs
.GroupBy(t => depths[t.name])
.OrderBy(g => g.Key)
.ToList();
- // Create one layer for each set of toggles at a given topological depth
for (int i = 0; i < togglesByDepth.Count; i++)
{
var depthGroup = togglesByDepth[i];
@@ -645,9 +518,7 @@ namespace YOTS
{
string paramName = toggle.name;
if (!genAnimatorConfig.parameters.Contains(paramName))
- {
genAnimatorConfig.parameters.Add(paramName);
- }
layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry {
name = toggle.name + "_On",
@@ -666,9 +537,60 @@ namespace YOTS
return genAnimatorConfig;
}
+ private static GeneratedAnimationsConfig GenerateAnimationConfig(List<ToggleSpec> toggleSpecs)
+ {
+ GeneratedAnimationsConfig genAnimConfig = new GeneratedAnimationsConfig();
+ foreach (var toggle in toggleSpecs)
+ {
+ GeneratedAnimationClipConfig onAnim = new GeneratedAnimationClipConfig();
+ onAnim.name = toggle.name + "_On";
+ if (toggle.meshToggles != null)
+ {
+ foreach (var mesh in toggle.meshToggles)
+ {
+ onAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 1.0f });
+ }
+ }
+ if (toggle.blendShapes != null)
+ {
+ foreach (var bs in toggle.blendShapes)
+ {
+ onAnim.blendShapes.Add(new GeneratedBlendShape {
+ path = bs.path,
+ blendShape = bs.blendShape,
+ value = bs.onValue
+ });
+ }
+ }
+ genAnimConfig.animations.Add(onAnim);
+
+ GeneratedAnimationClipConfig offAnim = new GeneratedAnimationClipConfig();
+ offAnim.name = toggle.name + "_Off";
+ if (toggle.meshToggles != null)
+ {
+ foreach (var mesh in toggle.meshToggles)
+ {
+ offAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 0.0f });
+ }
+ }
+ if (toggle.blendShapes != null)
+ {
+ foreach (var bs in toggle.blendShapes)
+ {
+ offAnim.blendShapes.Add(new GeneratedBlendShape {
+ path = bs.path,
+ blendShape = bs.blendShape,
+ value = bs.offValue
+ });
+ }
+ }
+ genAnimConfig.animations.Add(offAnim);
+ }
+ return genAnimConfig;
+ }
+
private static GeneratedAnimatorConfig ApplyIndependentFixToAnimatorConfig(GeneratedAnimatorConfig genAnimatorConfig)
{
- // Local helper functions to fetch the desired off value
float GetOffValueForMesh(string path, List<GeneratedMeshToggle> offList)
{
var offToggle = offList?.FirstOrDefault(mt => mt.path == path);
@@ -681,7 +603,6 @@ namespace YOTS
return offBlend != null ? offBlend.value : 0.0f;
}
- // Group paired animations by toggle name (extracted from the animation name by removing _On/_Off).
Dictionary<string, (GeneratedAnimationClipConfig on, GeneratedAnimationClipConfig off)> toggleAnimations =
new Dictionary<string, (GeneratedAnimationClipConfig, GeneratedAnimationClipConfig)>();
@@ -691,9 +612,7 @@ namespace YOTS
{
string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_On"));
if (!toggleAnimations.ContainsKey(toggleName))
- {
toggleAnimations[toggleName] = (null, null);
- }
var pair = toggleAnimations[toggleName];
pair.on = anim;
toggleAnimations[toggleName] = pair;
@@ -702,17 +621,13 @@ namespace YOTS
{
string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_Off"));
if (!toggleAnimations.ContainsKey(toggleName))
- {
toggleAnimations[toggleName] = (null, null);
- }
var pair = toggleAnimations[toggleName];
pair.off = anim;
toggleAnimations[toggleName] = pair;
}
}
- // Determine in which layer a given toggle exists.
- // (We assume that the base layer is named "BaseLayer" or is the first layer.)
Dictionary<string, int> toggleToLayerIndex = new Dictionary<string, int>();
for (int i = 0; i < genAnimatorConfig.layers.Count; i++)
{
@@ -720,40 +635,27 @@ namespace YOTS
foreach (var entry in layer.directBlendTree.entries)
{
string entryName = entry.name;
- // Remove any existing suffix (_On, _Off, _Independent, _Dependent) to get the base toggle.
string toggleName = entryName;
if (toggleName.EndsWith("_On"))
- {
toggleName = toggleName.Substring(0, toggleName.LastIndexOf("_On"));
- }
else if (toggleName.EndsWith("_Off"))
- {
toggleName = toggleName.Substring(0, toggleName.LastIndexOf("_Off"));
- }
else if (toggleName.Contains("_Independent"))
- {
toggleName = toggleName.Replace("_Independent", "");
- }
else if (toggleName.Contains("_Dependent"))
- {
toggleName = toggleName.Replace("_Dependent", "");
- }
if (!toggleToLayerIndex.ContainsKey(toggleName))
- {
toggleToLayerIndex[toggleName] = i;
- }
}
}
- // Build a global mapping from each affected attribute to the set of toggles that affect it.
- // For mesh toggles the key is "MeshToggle:{path}" and for blend shapes "BlendShape:{path}/{blendShape}".
Dictionary<string, HashSet<string>> attributeToToggles = new Dictionary<string, HashSet<string>>();
foreach (var kvp in toggleAnimations)
{
string toggleName = kvp.Key;
var pair = kvp.Value;
- if (pair.on == null) continue; // skip if missing
+ if (pair.on == null) continue;
HashSet<string> attributes = new HashSet<string>();
if (pair.on.meshToggles != null)
@@ -784,34 +686,26 @@ namespace YOTS
}
}
- // We will rebuild the animations list.
List<GeneratedAnimationClipConfig> newAnimations = new List<GeneratedAnimationClipConfig>();
- // Assume that the base layer is named "BaseLayer"; otherwise use the first layer.
AnimatorLayer baseLayer = genAnimatorConfig.layers.FirstOrDefault(l => l.name == "BaseLayer");
if (baseLayer == null && genAnimatorConfig.layers.Count > 0)
- {
baseLayer = genAnimatorConfig.layers[0];
- }
- // Process each toggle pair.
foreach (var kvp in toggleAnimations)
{
string toggleName = kvp.Key;
var pair = kvp.Value;
- // Determine the layer index in which this toggle appears.
int layerIndex = toggleToLayerIndex.ContainsKey(toggleName) ? toggleToLayerIndex[toggleName] : 0;
bool isBase = (layerIndex == 0);
if (isBase)
{
- // Base layer toggles remain unchanged.
newAnimations.Add(pair.on);
newAnimations.Add(pair.off);
}
else
{
- // For toggles in override layers we subdivide the affected attributes.
List<GeneratedMeshToggle> independentMesh = new List<GeneratedMeshToggle>();
List<GeneratedMeshToggle> dependentMesh = new List<GeneratedMeshToggle>();
@@ -821,13 +715,9 @@ namespace YOTS
{
string attr = "MeshToggle:" + mt.path;
if (attributeToToggles[attr].Count == 1)
- {
independentMesh.Add(mt);
- }
else
- {
dependentMesh.Add(mt);
- }
}
}
@@ -840,13 +730,9 @@ namespace YOTS
{
string attr = "BlendShape:" + bs.path + "/" + bs.blendShape;
if (attributeToToggles[attr].Count == 1)
- {
independentBlend.Add(bs);
- }
else
- {
dependentBlend.Add(bs);
- }
}
}
@@ -855,13 +741,11 @@ namespace YOTS
if (hasIndependent && hasDependent)
{
- // Create the dependent pair using the on values from the original config
GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
dependentOn.name = toggleName + "_Dependent_On";
dependentOn.meshToggles = dependentMesh;
dependentOn.blendShapes = dependentBlend;
- // Build the Off animation using the user-specified off values
GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig();
dependentOff.name = toggleName + "_Dependent_Off";
dependentOff.meshToggles = dependentMesh
@@ -878,7 +762,6 @@ namespace YOTS
})
.ToList();
- // Create the independent pair similarly.
GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig();
independentOn.name = toggleName + "_Independent_On";
independentOn.meshToggles = independentMesh;
@@ -905,25 +788,16 @@ namespace YOTS
newAnimations.Add(independentOn);
newAnimations.Add(independentOff);
- // Update the override layer's direct blend tree entries for this toggle.
AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
foreach (var entry in overrideLayer.directBlendTree.entries)
{
if (entry.name.StartsWith(toggleName) &&
(entry.name.EndsWith("_On") || entry.name.EndsWith("_Off")))
{
- if (entry.name.EndsWith("_On"))
- {
- entry.name = toggleName + "_Dependent_On";
- }
- else
- {
- entry.name = toggleName + "_Dependent_Off";
- }
+ entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
}
}
- // In the base layer, append new direct blend tree entries for the independent pair.
if (baseLayer != null)
{
baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry
@@ -940,7 +814,6 @@ namespace YOTS
}
else if (hasIndependent && !hasDependent)
{
- // All affected attributes are independent.
GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig();
independentOn.name = toggleName + "_Independent_On";
independentOn.meshToggles = pair.on.meshToggles;
@@ -953,7 +826,6 @@ namespace YOTS
newAnimations.Add(independentOn);
newAnimations.Add(independentOff);
- // Remove the entries from the override layer and add new entries to the base layer.
AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
overrideLayer.directBlendTree.entries.RemoveAll(e => e.name.StartsWith(toggleName));
if (baseLayer != null)
@@ -972,7 +844,6 @@ namespace YOTS
}
else if (!hasIndependent && hasDependent)
{
- // All affected attributes are shared.
GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
dependentOn.name = toggleName + "_Dependent_On";
dependentOn.meshToggles = pair.on.meshToggles;
@@ -985,29 +856,19 @@ namespace YOTS
newAnimations.Add(dependentOn);
newAnimations.Add(dependentOff);
- // Update the override layer's entries.
AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
foreach (var entry in overrideLayer.directBlendTree.entries)
{
if (entry.name.StartsWith(toggleName) &&
(entry.name.EndsWith("_On") || entry.name.EndsWith("_Off")))
{
- if (entry.name.EndsWith("_On"))
- {
- entry.name = toggleName + "_Dependent_On";
- }
- else
- {
- entry.name = toggleName + "_Dependent_Off";
- }
+ entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
}
}
}
- // If there are no affected attributes, nothing is added.
}
}
- // Replace the animations list in the config.
genAnimatorConfig.animations = newAnimations;
return genAnimatorConfig;
}
@@ -1019,23 +880,18 @@ namespace YOTS
var layer = config.layers[i];
layer.directBlendTree.entries.RemoveAll(entry => entry.name.EndsWith("_Off"));
}
-
return config;
}
private static GeneratedAnimatorConfig RemoveUnusedAnimations(GeneratedAnimatorConfig config)
{
- // Collect all animation names referenced in blend tree entries across all layers
HashSet<string> referencedAnimations = new HashSet<string>();
foreach (var layer in config.layers)
{
foreach (var entry in layer.directBlendTree.entries)
- {
referencedAnimations.Add(entry.name);
- }
}
- // Filter the animations list to keep only referenced animations
config.animations = config.animations
.Where(anim => referencedAnimations.Contains(anim.name))
.ToList();
@@ -1044,33 +900,35 @@ namespace YOTS
return config;
}
- private static VRCExpressionsMenu GetOrCreateSubmenu(VRCExpressionsMenu parentMenu, string submenuName, string generatedDir)
+ private static VRCExpressionsMenu GetOrCreateSubmenu(
+ VRCExpressionsMenu parentMenu,
+ string submenuName,
+ Action<UnityEngine.Object> saveAsset
+ )
{
- Debug.Log($"Getting or creating submenu {submenuName} under {parentMenu.name} at {generatedDir}");
if (parentMenu.controls == null)
parentMenu.controls = new List<VRCExpressionsMenu.Control>();
- // Look for an existing submenu control with the given name.
+ // Check if submenu already exists
foreach (var control in parentMenu.controls)
{
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
control.name == submenuName && control.subMenu != null)
{
- return control.subMenu;
+ // Clone existing submenu to avoid modifying original
+ var clonedSubmenu = UnityEngine.Object.Instantiate(control.subMenu);
+ saveAsset(clonedSubmenu);
+ control.subMenu = clonedSubmenu;
+ return clonedSubmenu;
}
}
- // Not found; create a new submenu asset.
+ // Create new submenu
var newSubmenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
newSubmenu.name = submenuName;
- if (newSubmenu.controls == null)
- newSubmenu.controls = new List<VRCExpressionsMenu.Control>();
-
- string submenuAssetPath = Path.Combine(generatedDir, submenuName + "_Submenu.asset");
- AssetDatabase.CreateAsset(newSubmenu, submenuAssetPath);
- AssetDatabase.SaveAssets();
+ newSubmenu.controls = new List<VRCExpressionsMenu.Control>();
+ saveAsset(newSubmenu);
- // Add a control in the parent menu for the submenu.
var newControl = new VRCExpressionsMenu.Control
{
name = submenuName,
@@ -1078,9 +936,6 @@ namespace YOTS
subMenu = newSubmenu
};
parentMenu.controls.Add(newControl);
- EditorUtility.SetDirty(parentMenu);
- EditorUtility.SetDirty(newSubmenu);
- Debug.Log($"Created submenu '{submenuName}' at {submenuAssetPath}");
return newSubmenu;
}
@@ -1089,16 +944,12 @@ namespace YOTS
{
if (menu == null) return;
- // Clear existing controls
if (menu.controls != null)
{
- // Recursively initialize any existing submenus before removing them
foreach (var control in menu.controls)
{
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null)
- {
InitializeSubmenu(control.subMenu);
- }
}
menu.controls.Clear();
}
@@ -1106,116 +957,76 @@ namespace YOTS
{
menu.controls = new List<VRCExpressionsMenu.Control>();
}
-
- EditorUtility.SetDirty(menu);
}
- private static void GenerateVRChatAssets(List<ToggleSpec> toggleSpecs, string generatedDir, string existingParamsPath = null, string existingMenuPath = null)
+ private static void GenerateVRChatAssets(
+ List<ToggleSpec> toggleSpecs,
+ Action<UnityEngine.Object> saveAsset,
+ VRCExpressionParameters existingParams = null,
+ VRCExpressionsMenu mainMenu = null
+ )
{
- // Create a unique list of toggle specs for adding parameters
var uniqueToggles = toggleSpecs
- .Where(t => t.name != "YOTS_Weight") // Skip the special YOTS_Weight parameter
+ .Where(t => t.name != "YOTS_Weight")
.GroupBy(t => t.name)
.Select(g => g.First())
.ToList();
- // Create or update the VRC Expression Parameters asset.
VRCExpressionParameters expressionParameters;
- if (string.IsNullOrEmpty(existingParamsPath)) {
+ if (existingParams == null) {
expressionParameters = ScriptableObject.CreateInstance<VRCExpressionParameters>();
} else {
- expressionParameters = AssetDatabase.LoadAssetAtPath<VRCExpressionParameters>(existingParamsPath);
- if (expressionParameters == null) {
- Debug.LogError($"Could not load existing parameters at path: {existingParamsPath}");
- return;
- }
+ expressionParameters = existingParams;
}
- // Merge existing parameters with new toggle parameters.
var paramList = new List<VRCExpressionParameters.Parameter>();
if (expressionParameters.parameters != null) {
- // Remove any parameters that have the same name as our unique toggles
paramList.AddRange(expressionParameters.parameters.Where(p => !uniqueToggles.Any(t => t.name == p.name)));
}
foreach (var toggle in uniqueToggles)
{
- // For the VRChat parameters: toggles become booleans and radials stay floats.
paramList.Add(new VRCExpressionParameters.Parameter
{
name = toggle.name,
valueType = toggle.type == "radial" ? VRCExpressionParameters.ValueType.Float : VRCExpressionParameters.ValueType.Bool,
- // defaultValue of 0 means "false" for a toggle; adjust as desired for sliders.
defaultValue = 0f,
saved = true
});
}
expressionParameters.parameters = paramList.ToArray();
- // Handle the main menu: if an existing menu asset was provided, update it.
- VRCExpressionsMenu mainMenu;
- if (string.IsNullOrEmpty(existingMenuPath))
- {
- mainMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
- if (mainMenu.controls == null)
- mainMenu.controls = new List<VRCExpressionsMenu.Control>();
- }
- else
- {
- mainMenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(existingMenuPath);
- if (mainMenu == null)
- {
- Debug.LogError($"Could not load existing menu at path: {existingMenuPath}");
- return;
- }
- // Remove any existing YOTS submenu from the main menu.
- mainMenu.controls.RemoveAll(c => c.name == "YOTS");
- }
+ mainMenu.controls.RemoveAll(c => c.name == "YOTS");
- // Create or load the root YOTS submenu
- string yotsSubmenuPath = Path.Combine(generatedDir, "YOTS_Submenu.asset");
- VRCExpressionsMenu yotsSubmenu;
-
- if (AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(yotsSubmenuPath) != null)
- {
- yotsSubmenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(yotsSubmenuPath);
- InitializeSubmenu(yotsSubmenu);
- Debug.Log("Reset existing YOTS submenu at: " + yotsSubmenuPath);
- }
- else
- {
- yotsSubmenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
- yotsSubmenu.name = "YOTS";
- yotsSubmenu.controls = new List<VRCExpressionsMenu.Control>();
- AssetDatabase.CreateAsset(yotsSubmenu, yotsSubmenuPath);
- Debug.Log("Created new YOTS submenu at: " + yotsSubmenuPath);
- }
+ // Create YOTS submenu
+ VRCExpressionsMenu yotsSubmenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
+ yotsSubmenu.name = "YOTS";
+ yotsSubmenu.controls = new List<VRCExpressionsMenu.Control>();
+ saveAsset(yotsSubmenu);
+
+ // Track all created/modified menus to ensure they're saved
+ HashSet<VRCExpressionsMenu> modifiedMenus = new HashSet<VRCExpressionsMenu> { mainMenu, yotsSubmenu };
- // For each toggle, determine where its control should live based on its menuPath.
foreach (var toggle in toggleSpecs)
{
VRCExpressionsMenu currentMenu = yotsSubmenu;
- // A menuPath of "/" (or an empty string) means "stay at the root."
if (!string.IsNullOrEmpty(toggle.menuPath) && toggle.menuPath != "/")
{
- // Remove extra slashes and split into sections.
string trimmedPath = toggle.menuPath.Trim('/');
var sections = trimmedPath.Split('/');
- // Recursively get or create each submenu.
foreach (var section in sections)
{
- currentMenu = GetOrCreateSubmenu(currentMenu, section, generatedDir);
+ currentMenu = GetOrCreateSubmenu(currentMenu, section, saveAsset);
+ modifiedMenus.Add(currentMenu);
}
}
- // Create a control based on toggle type.
+ // Add toggle controls
if (toggle.type == "radial")
{
- // Radial puppet
currentMenu.controls.Add(new VRCExpressionsMenu.Control
{
name = toggle.name,
type = VRCExpressionsMenu.Control.ControlType.RadialPuppet,
- // Shoutsout av3emulator/Lyuma for showing how to do this.
subParameters = new VRCExpressionsMenu.Control.Parameter[] {
new VRCExpressionsMenu.Control.Parameter { name = toggle.name }
}
@@ -1223,7 +1034,6 @@ namespace YOTS
}
else
{
- // Standard toggle
currentMenu.controls.Add(new VRCExpressionsMenu.Control
{
name = toggle.name,
@@ -1232,10 +1042,9 @@ namespace YOTS
value = 1f
});
}
- EditorUtility.SetDirty(currentMenu);
}
- // Add the complete YOTS submenu as a control in the main menu.
+ // Add YOTS submenu to main menu
mainMenu.controls.Add(new VRCExpressionsMenu.Control
{
name = "YOTS",
@@ -1243,121 +1052,16 @@ namespace YOTS
subMenu = yotsSubmenu
});
- // Save the assets.
- if (string.IsNullOrEmpty(existingParamsPath))
- {
- string paramPath = Path.Combine(generatedDir, "YOTS_Parameters.asset");
- AssetDatabase.CreateAsset(expressionParameters, paramPath);
- Debug.Log($"Generated new VRChat parameters at: {paramPath}");
- }
- else
- {
- EditorUtility.SetDirty(expressionParameters);
- Debug.Log($"Updated existing VRChat parameters at: {existingParamsPath}");
- }
-
- if (string.IsNullOrEmpty(existingMenuPath))
- {
- string mainMenuPath = Path.Combine(generatedDir, "YOTS_Menu.asset");
- AssetDatabase.CreateAsset(mainMenu, mainMenuPath);
- Debug.Log($"Generated new VRChat menu at: {mainMenuPath}");
- }
- else
- {
- EditorUtility.SetDirty(mainMenu);
- Debug.Log($"Updated existing VRChat menu at: {existingMenuPath}");
- }
- }
-
- public static void GenerateAnimator(string configPath = null, string animatorName = "YOTS_FX", string existingParamsPath = null, string existingMenuPath = null)
- {
- Debug.Log("=== Starting Animator Generation Process ===");
-
- if (string.IsNullOrEmpty(configPath))
- {
- configPath = EditorUtility.OpenFilePanel("Select Animator Config JSON", Application.dataPath, "json");
- if (string.IsNullOrEmpty(configPath))
- {
- Debug.LogError("No configuration file selected. Process aborted.");
- return;
- }
- }
- Debug.Log("Loading configuration from: " + configPath);
-
- string jsonContent = File.ReadAllText(configPath);
- AnimatorConfigFile config;
- try
+ // Save all modified menus
+ foreach (var menu in modifiedMenus)
{
- config = JsonUtility.FromJson<AnimatorConfigFile>(jsonContent);
+ saveAsset(menu);
}
- catch (System.Exception e)
- {
- Debug.LogError($"JSON parsing failed: {e.Message}");
- return;
- }
- if (config == null)
- {
- Debug.LogError("Configuration file is empty or invalid");
- return;
- }
-
- if (config.toggles == null)
- {
- Debug.LogError("No toggleSpecs found in configuration");
- return;
- }
- Debug.Log($"Configuration loaded. Found {config.toggles.Count} toggles.");
- // Ensure all output directories exist
- string generatedDir = Path.Combine("Assets", "YOTS_Generated");
- string fullGeneratedDir = Path.Combine(Application.dataPath, "YOTS_Generated");
- string fullAnimationsDir = Path.Combine(fullGeneratedDir, "Animations");
-
- if (!Directory.Exists(fullGeneratedDir))
+ if (existingParams == null)
{
- Directory.CreateDirectory(fullGeneratedDir);
- Debug.Log("Created config output directory: " + fullGeneratedDir);
+ saveAsset(expressionParameters);
}
- if (!Directory.Exists(fullAnimationsDir))
- {
- Directory.CreateDirectory(fullAnimationsDir);
- Debug.Log("Created animations output directory: " + fullAnimationsDir);
- }
-
- // First we generate a naive animator config. We topologically sort
- // toggles according to their dependencies and place them into
- // layers. Everything is structured as an On/Off pair of
- // animations, even though this is only semantically correct
- // for the base layer.
- GeneratedAnimatorConfig genAnimatorConfig = GenerateNaiveAnimatorConfig(config.toggles, animatorName);
- // Next we split animations into independent and dependent parts.
- // Independent parts are melded into the base layer.
- genAnimatorConfig = ApplyIndependentFixToAnimatorConfig(genAnimatorConfig);
- // Next we restructure the override layers as simple "On"
- // animations which override the state inherited from previous layers.
- genAnimatorConfig = RemoveOffAnimationsFromOverrideLayers(genAnimatorConfig);
- // Finally, we scrub out any animations which may have been orphaned.
- genAnimatorConfig = RemoveUnusedAnimations(genAnimatorConfig);
-
- // Generate VRChat parameters and menu
- GenerateVRChatAssets(config.toggles, generatedDir, existingParamsPath, existingMenuPath);
-
- // Save the generated animator configuration JSON file. This is for
- // debuggability.
- string genAnimatorConfigPath = Path.Combine(fullGeneratedDir, "gen_animator.json");
- File.WriteAllText(genAnimatorConfigPath, JsonUtility.ToJson(genAnimatorConfig, true));
- Debug.Log("Saved generated animator config to: " + genAnimatorConfigPath);
-
- // Create the animation clips directly from the animator config
- CreateAnimationClips(new GeneratedAnimationsConfig { animations = genAnimatorConfig.animations }, generatedDir);
-
- // Generate the animator controller
- GenerateAnimatorController(genAnimatorConfig, generatedDir);
-
- AssetDatabase.SaveAssets();
- AssetDatabase.Refresh();
-
- Debug.Log("=== Animator Generation Process Complete ===");
}
}
}
diff --git a/YOTSNDMFConfig.cs b/YOTSNDMFConfig.cs
new file mode 100644
index 0000000..baf92b8
--- /dev/null
+++ b/YOTSNDMFConfig.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+using VRC.SDK3.Avatars.ScriptableObjects;
+
+namespace YOTS
+{
+ public class YOTSNDMFConfig : MonoBehaviour
+ {
+ [Tooltip("The JSON configuration file.")]
+ public TextAsset jsonConfig;
+ }
+}
diff --git a/YOTSNDMFGenerator.cs b/YOTSNDMFGenerator.cs
new file mode 100644
index 0000000..7ea20c8
--- /dev/null
+++ b/YOTSNDMFGenerator.cs
@@ -0,0 +1,91 @@
+using UnityEngine;
+using nadena.dev.ndmf;
+using nadena.dev.ndmf.VRChat;
+using nadena.dev.ndmf.localization;
+using VRC.SDK3.Avatars.ScriptableObjects;
+using UnityEditor;
+using System;
+using System.Collections.Generic;
+
+[assembly: ExportsPlugin(typeof(YOTS.YOTSNDMFGenerator))]
+
+namespace YOTS
+{
+ public class YOTSNDMFGenerator : Plugin<YOTSNDMFGenerator>
+ {
+ private readonly Localizer localizer = new Localizer("en-us", () => new List<(string, Func<string, string>)>
+ {
+ ("en-us", key => key) // Simple pass-through for English
+ });
+
+ public override string DisplayName => "YOTS Animator Generator";
+
+ protected override void Configure()
+ {
+ // Use a different pass name in play mode to indicate temporary processing.
+ InPhase(BuildPhase.Transforming)
+ .Run("Generate YOTS Animator", ctx => {
+ // ctx is a BuildContext
+ // https://ndmf.nadena.dev/api/nadena.dev.ndmf.BuildContext.html
+ // Get config
+ var config = ctx.AvatarRootObject.GetComponentInChildren<YOTSNDMFConfig>();
+ if (config == null) {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.no_config",
+ "No YOTS config component found on the avatar.");
+ return;
+ }
+ if (config.jsonConfig == null) {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.no_json",
+ "YOTS config component is missing JSON config file.");
+ return;
+ }
+ // Get descriptor
+ var descriptor = ctx.AvatarDescriptor;
+ if (descriptor == null) {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.no_descriptor",
+ "Avatar descriptor is missing.");
+ return;
+ }
+ RuntimeAnimatorController animator = descriptor.baseAnimationLayers[4].animatorController;
+ if (animator == null) {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.no_animator",
+ "FX layer is missing.");
+ return;
+ }
+ var menu = descriptor.expressionsMenu;
+ var parameters = descriptor.expressionParameters;
+ if (parameters == null || menu == null)
+ {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.missing_assets",
+ "Avatar parameters or menu is missing.");
+ return;
+ }
+ // Create a clone of the menu and parameters and save them
+ menu = UnityEngine.Object.Instantiate(menu);
+ parameters = UnityEngine.Object.Instantiate(parameters);
+ ctx.AssetSaver.SaveAsset(menu);
+ ctx.AssetSaver.SaveAsset(parameters);
+ descriptor.expressionsMenu = menu;
+ descriptor.expressionParameters = parameters;
+ // Generate the animator asset using the BuildContext's AssetSaver
+ RuntimeAnimatorController generatedAnimator = YOTSCore.GenerateAnimator(
+ config.jsonConfig.text,
+ parameters,
+ menu,
+ ctx.AssetSaver.SaveAsset
+ );
+ if (generatedAnimator == null) {
+ ErrorReport.ReportError(localizer, ErrorSeverity.Error, "yots.error.generation_failed",
+ "Failed to generate animator controller.");
+ return;
+ }
+ // TODO merge animators
+ descriptor.baseAnimationLayers[4].animatorController = generatedAnimator;
+
+ // During play mode, apply additional temporary processing to the avatar.
+ //AvatarProcessor.ProcessAvatar(ctx.AvatarRootObject);
+ }
+ );
+ }
+ }
+}