diff options
| -rw-r--r-- | README.md | 13 | ||||
| -rw-r--r-- | Scripts/YOTSCore.cs | 484 | ||||
| -rw-r--r-- | Scripts/YOTSNDMFConfig.cs | 6 | ||||
| -rw-r--r-- | Scripts/YOTSNDMFGenerator.cs | 33 |
4 files changed, 199 insertions, 337 deletions
@@ -26,7 +26,6 @@ Open your text editor of choice and paste this in: { "name": "Shirt", "meshToggles": ["Shirt"], - "blendShapes": [] } ] } @@ -37,7 +36,7 @@ Feel free to replace "Shirt" with the name of some other mesh on your avatar. Save it to Assets/animator.json. Drag Assets/yum\_food/YOTS.prefab anywhere on your avatar. Select it in the -hierarchy, and drag Assets/animator.json onto the "JSON config" field. +hierarchy, and drag Assets/animator.json onto the "Json Config" field. Enter play mode. Enable an emulator (I use Lyuma's av3emulator). Open your menu. You should see a YOTS submenu. Click it, then click Shirt. Your shirt @@ -52,13 +51,14 @@ A logical sequence of things to try: 3. Declare a dependency on another toggle with `"dependencies": ["someOtherToggle"]`. 4. Install a toggle at a custom path with `"menuPath": "/my/custom/path"`. 5. Add a radial puppet with `"type": "radial"`. +6. Use `"defaultValue": 0.0` to set a toggle to off by default. Toggle options are documented in two places: -1. Read the ToggleSpec definition at the top of +1. The ToggleSpec definition at the top of [YOTSCore.cs](./Scripts/YOTSCore.cs). This is the definitive source of truth. -2. Skim the examples under Examples/ +2. The [Examples](./Examples). ## Motivation @@ -87,7 +87,7 @@ the lifetime of an avatar: 1. Adding new articles of clothing. You now have to edit all your existing avatar-wide animations to include them. -2. Adding new kinds of toggles or sliders. You may want them to affect a large +2. Adding new toggles or sliders. You may want them to affect a large set of items. For example: you added a minimum brightness slider, and now have to animate 20 different articles of clothing. 3. Removing articles of clothing. You should remove them from your avatar-wide @@ -95,8 +95,7 @@ the lifetime of an avatar: 4. Removing toggles or sliders. It's easy to accidentally orphan an animator layer, or a parameter somewhere. -These are all vastly easier to perform through a textual configuration system -than through VRChat's native GUI approach. +These are all vastly easier to perform through YOTS. ## Design derivation diff --git a/Scripts/YOTSCore.cs b/Scripts/YOTSCore.cs index 5ee3752..3cfb699 100644 --- a/Scripts/YOTSCore.cs +++ b/Scripts/YOTSCore.cs @@ -13,13 +13,12 @@ using VRC.SDK3.Avatars.ScriptableObjects; namespace YOTS
{
[System.Serializable]
- public class ToggleSpec
- {
+ public class ToggleSpec {
// The name of the toggle. This is plumbed into the menu, the VRChat
// parameters, and the animator parameters.
[SerializeField]
public string name;
-
+
// The type of toggle.
// Accepted values:
// "toggle" - A boolean toggle. Creates a boolean sync param.
@@ -54,35 +53,30 @@ namespace YOTS [SerializeField]
public float defaultValue = 1.0f;
- public ToggleSpec(string name)
- {
+ public ToggleSpec(string name) {
this.name = name;
}
public ToggleSpec() {}
- public IEnumerable<string> GetAffectedAttributes()
- {
- foreach (var mesh in meshToggles)
- {
+ public IEnumerable<string> GetAffectedAttributes() {
+ foreach (var mesh in meshToggles) {
yield return $"MeshToggle:{mesh}";
}
- foreach (var blend in blendShapes)
- {
+ foreach (var blend in blendShapes) {
yield return $"BlendShape:{blend.path}/{blend.blendShape}";
}
}
}
[System.Serializable]
- public class BlendShapeSpec
- {
+ public class BlendShapeSpec {
// The path to the mesh renderer to apply the blend shape to.
// For example, "Body" or "Shirt".
[SerializeField]
public string path;
-
+
// The name of the blend shape to apply.
// For example, "Chest_Hide" or "Boobs+".
[SerializeField]
@@ -96,8 +90,7 @@ namespace YOTS [SerializeField]
public float onValue = 100.0f;
- public BlendShapeSpec(string path, string blendShape, float offValue = 0, float onValue = 100)
- {
+ public BlendShapeSpec(string path, string blendShape, float offValue = 0, float onValue = 100) {
this.path = path;
this.blendShape = blendShape;
this.offValue = offValue;
@@ -108,8 +101,7 @@ namespace YOTS }
[System.Serializable]
- public class AnimatorConfigFile
- {
+ public class AnimatorConfigFile {
[SerializeField]
public List<ToggleSpec> toggles = new List<ToggleSpec>();
@@ -118,15 +110,13 @@ namespace YOTS }
[System.Serializable]
- public class GeneratedAnimationsConfig
- {
+ public class GeneratedAnimationsConfig {
public List<GeneratedAnimationClipConfig> animations =
new List<GeneratedAnimationClipConfig>();
}
[System.Serializable]
- public class GeneratedAnimationClipConfig
- {
+ public class GeneratedAnimationClipConfig {
public string name;
public List<GeneratedMeshToggle> meshToggles =
new List<GeneratedMeshToggle>();
@@ -135,15 +125,13 @@ namespace YOTS }
[System.Serializable]
- public class GeneratedMeshToggle
- {
+ public class GeneratedMeshToggle {
public string path;
public float value;
}
[System.Serializable]
- public class GeneratedBlendShape
- {
+ public class GeneratedBlendShape {
public string path;
public string blendShape;
public float value;
@@ -151,8 +139,7 @@ namespace YOTS // These classes describe the generated JSON output for the animator configuration.
[System.Serializable]
- public class GeneratedAnimatorConfig
- {
+ public class GeneratedAnimatorConfig {
public List<AnimatorParameterSetting> parameters = new List<AnimatorParameterSetting>();
public List<AnimatorLayer> layers = new List<AnimatorLayer>();
public List<GeneratedAnimationClipConfig> animations =
@@ -160,117 +147,94 @@ namespace YOTS }
[System.Serializable]
- public class AnimatorLayer
- {
+ public class AnimatorLayer {
public string name;
public AnimatorDirectBlendTree directBlendTree =
new AnimatorDirectBlendTree();
}
[System.Serializable]
- public class AnimatorDirectBlendTree
- {
+ public class AnimatorDirectBlendTree {
public List<AnimatorDirectBlendTreeEntry> entries =
new List<AnimatorDirectBlendTreeEntry>();
}
[System.Serializable]
- public class AnimatorDirectBlendTreeEntry
- {
+ public class AnimatorDirectBlendTreeEntry {
public string name; // animation name
public string parameter; // parameter driving the animation
}
// Add these new classes at the namespace level
[System.Serializable]
- public class VRCMenuConfig
- {
+ public class VRCMenuConfig {
public string menuName = "YOTS";
public List<VRCMenuItemConfig> items = new List<VRCMenuItemConfig>();
}
[System.Serializable]
- public class VRCMenuItemConfig
- {
+ public class VRCMenuItemConfig {
public string name;
public string parameter;
public Texture2D icon;
}
[System.Serializable]
- public class AnimatorParameterSetting
- {
+ public class AnimatorParameterSetting {
public string name;
public float defaultValue;
}
- public class YOTSCore
- {
- private static Dictionary<string, AnimationClip> animationClipCache = new Dictionary<string, AnimationClip>();
+ public class YOTSCore {
+ private static Dictionary<string, AnimationClip> animationClips = new Dictionary<string, AnimationClip>();
public static AnimatorController GenerateAnimator(
string configJson,
VRCExpressionParameters vrcParams,
VRCExpressionsMenu vrcMenu
- )
- {
+ ) {
Debug.Log("=== Starting Animator Generation Process ===");
- if (string.IsNullOrEmpty(configJson))
- {
+ if (string.IsNullOrEmpty(configJson)) {
throw new ArgumentException("No config JSON provided.");
}
- Debug.Log("Parsing JSON configuration");
AnimatorConfigFile config;
- try
- {
+ try {
config = JsonUtility.FromJson<AnimatorConfigFile>(configJson);
}
- catch (System.Exception e)
- {
+ catch (System.Exception e) {
throw new ArgumentException($"JSON parsing failed: {e.Message}");
}
- if (config == null)
- {
+ if (config == null) {
throw new ArgumentException("JSON config is empty or invalid");
}
-
- if (config.toggles == null)
- {
+
+ if (config.toggles == null) {
throw new ArgumentException("No toggleSpecs found in configuration");
}
Debug.Log($"Configuration loaded. Found {config.toggles.Count} toggles.");
-
+ // Create abstract representation of the animator.
GeneratedAnimatorConfig genAnimatorConfig = GenerateNaiveAnimatorConfig(config.toggles);
genAnimatorConfig = ApplyIndependentFixToAnimatorConfig(genAnimatorConfig);
genAnimatorConfig = RemoveOffAnimationsFromOverrideLayers(genAnimatorConfig);
genAnimatorConfig = RemoveUnusedAnimations(genAnimatorConfig);
-
- // Generate VRChat parameters and menu
+ // Create actual assets.
GenerateVRChatAssets(config.toggles, vrcParams, vrcMenu);
-
- // 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);
Debug.Log("=== Animator Generation Process Complete ===");
return controller;
}
- private static void CreateAnimationClips(GeneratedAnimationsConfig animationsConfig)
- {
- foreach (var clipConfig in animationsConfig.animations)
- {
+ private static void CreateAnimationClips(GeneratedAnimationsConfig animationsConfig) {
+ foreach (var clipConfig in animationsConfig.animations) {
AnimationClip newClip = new AnimationClip();
newClip.name = clipConfig.name;
// Apply mesh toggles
- foreach (var meshToggle in clipConfig.meshToggles)
- {
+ foreach (var meshToggle in clipConfig.meshToggles) {
AnimationCurve curve = new AnimationCurve(new Keyframe(0, meshToggle.value));
EditorCurveBinding binding = new EditorCurveBinding();
binding.path = meshToggle.path;
@@ -280,8 +244,7 @@ namespace YOTS }
// Apply blend shapes
- foreach (var blendShape in clipConfig.blendShapes)
- {
+ foreach (var blendShape in clipConfig.blendShapes) {
AnimationCurve curve = AnimationCurve.Constant(0, 0, blendShape.value);
EditorCurveBinding binding = new EditorCurveBinding();
binding.path = blendShape.path;
@@ -291,13 +254,12 @@ namespace YOTS }
// Store in memory cache
- animationClipCache[clipConfig.name] = newClip;
- Debug.Log("Created animation clip in memory: " + clipConfig.name);
+ animationClips[clipConfig.name] = newClip;
+ Debug.Log("Created animation clip " + clipConfig.name);
}
}
- private static AnimatorController GenerateAnimatorController(GeneratedAnimatorConfig animatorConfig)
- {
+ private static AnimatorController GenerateAnimatorController(GeneratedAnimatorConfig animatorConfig) {
AnimatorController controller = new AnimatorController();
// Add weight parameter used to ensure that the blendtrees always
// run. All layers use this. Documented on vrc.school:
@@ -309,8 +271,7 @@ namespace YOTS yots_weight.defaultFloat = 1.0f;
parameters_list.Add(yots_weight);
// Add all other parameters
- foreach (var param in animatorConfig.parameters)
- {
+ foreach (var param in animatorConfig.parameters) {
var p = new AnimatorControllerParameter();
p.name = param.name;
p.type = AnimatorControllerParameterType.Float;
@@ -319,70 +280,72 @@ namespace YOTS }
controller.parameters = parameters_list.ToArray();
- // Add base layer
- var baseLayer = animatorConfig.layers[0];
+ // Add base layer. This is structured as a wide direct blendtree
+ // (DBT) comprised of blendtrees animating pairs of On/Off
+ // animations.
+ var baseLayerConfig = animatorConfig.layers[0];
var baseStateMachine = new AnimatorStateMachine();
- baseStateMachine.name = "BaseLayer_SM";
+ baseStateMachine.name = "YOTS_BaseLayer_SM";
var rootBlendTree = new BlendTree();
- rootBlendTree.name = "BaseLayer_RootBlendTree";
+ rootBlendTree.name = "YOTS_BaseLayer_RootBlendTree";
rootBlendTree.blendType = BlendTreeType.Direct;
- var parameterGroups = baseLayer.directBlendTree.entries
+ var parameterGroups = baseLayerConfig.directBlendTree.entries
.GroupBy(e => e.parameter)
.ToDictionary(g => g.Key, g => g.ToList());
- foreach (var group in parameterGroups)
- {
+ // Iterate over (parameter, animationSet) pairs in the base layer.
+ foreach (var group in parameterGroups) {
var param = group.Key;
- var entries = group.Value;
+ var animations = group.Value;
+ // Create a blendtree controlled by this toggle's parameter.
var paramBlendTree = new BlendTree();
- paramBlendTree.name = $"BlendTree_{param}";
+ paramBlendTree.name = $"YOTS_BlendTree_{param}";
paramBlendTree.blendType = BlendTreeType.Simple1D;
paramBlendTree.blendParameter = param;
var children = new List<ChildMotion>();
- foreach (var entry in entries.OrderBy(e => e.name.EndsWith("_On")))
- {
- Debug.Log("Adding child motion for: " + entry.name);
- if (!animationClipCache.TryGetValue(entry.name, out AnimationClip clip))
- {
- throw new InvalidOperationException($"Animation clip not found in memory: {entry.name}");
+ foreach (var animation in animations.OrderBy(e => e.name.EndsWith("_On"))) {
+ Debug.Log("Adding child motion for: " + animation.name);
+ if (!animationClips.TryGetValue(animation.name, out AnimationClip clip)) {
+ throw new InvalidOperationException($"Animation clip not found in memory: {animation.name}");
}
- children.Add(new ChildMotion
- {
+ children.Add(new ChildMotion{
motion = clip,
timeScale = 1f,
- threshold = entry.name.EndsWith("_On") ? 1f : 0f
+ threshold = animation.name.EndsWith("_On") ? 1f : 0f
});
}
paramBlendTree.children = children.ToArray();
- rootBlendTree.children = rootBlendTree.children.Append(new ChildMotion
- {
- motion = paramBlendTree,
- timeScale = 1f,
- directBlendParameter = "YOTS_Weight"
+ // Add that blendtree to the parent direct blendtree (DBT)
+ // controlled by YOTS_Weight. That YOTS_Weight parameter is
+ // always set to 1, so the child blendtree always runs.
+ rootBlendTree.children = rootBlendTree.children.Append(
+ new ChildMotion{
+ motion = paramBlendTree,
+ timeScale = 1f,
+ directBlendParameter = "YOTS_Weight"
}).ToArray();
}
- var baseState = baseStateMachine.AddState("BaseLayer_State");
+ var baseState = baseStateMachine.AddState("YOTS_BaseLayer_State");
baseState.motion = rootBlendTree;
baseState.writeDefaultValues = true;
baseStateMachine.defaultState = baseState;
- controller.AddLayer(new AnimatorControllerLayer
- {
+ controller.AddLayer(new AnimatorControllerLayer{
name = "YOTS_BaseLayer",
defaultWeight = 1.0f,
stateMachine = baseStateMachine
});
- // Add override layers
- for (int i = 1; i < animatorConfig.layers.Count; i++)
- {
+ // Add override layers. These are DBTs of On animations (no Off
+ // animations).
+ for (int i = 1; i < animatorConfig.layers.Count; i++) {
var layerConfig = animatorConfig.layers[i];
string layerName = $"YOTS_OverrideLayer{(i-1).ToString("00")}";
@@ -393,15 +356,12 @@ namespace YOTS blendTree.name = layerName + "_BlendTree";
blendTree.blendType = BlendTreeType.Direct;
- foreach (var entry in layerConfig.directBlendTree.entries)
- {
- if (!animationClipCache.TryGetValue(entry.name, out AnimationClip clip))
- {
+ foreach (var entry in layerConfig.directBlendTree.entries) {
+ if (!animationClips.TryGetValue(entry.name, out AnimationClip clip)) {
throw new InvalidOperationException($"Animation clip not found in memory: {entry.name}");
}
-
- blendTree.children = blendTree.children.Append(new ChildMotion
- {
+
+ blendTree.children = blendTree.children.Append(new ChildMotion{
motion = clip,
timeScale = 1f,
directBlendParameter = entry.parameter
@@ -413,8 +373,7 @@ namespace YOTS state.writeDefaultValues = true;
stateMachine.defaultState = state;
- controller.AddLayer(new AnimatorControllerLayer
- {
+ controller.AddLayer(new AnimatorControllerLayer{
name = layerName,
defaultWeight = 1.0f,
stateMachine = stateMachine
@@ -426,15 +385,12 @@ namespace YOTS return controller;
}
- private static Dictionary<string, int> TopologicalSortToggles(List<ToggleSpec> toggleSpecs)
- {
+ private static Dictionary<string, int> TopologicalSortToggles(List<ToggleSpec> toggleSpecs) {
Dictionary<string, HashSet<string>> graph = new Dictionary<string, HashSet<string>>();
- foreach (var toggle in toggleSpecs)
- {
+ foreach (var toggle in toggleSpecs) {
if (!graph.ContainsKey(toggle.name))
graph[toggle.name] = new HashSet<string>();
- foreach (var dep in toggle.dependencies)
- {
+ foreach (var dep in toggle.dependencies) {
if (!graph.ContainsKey(dep))
graph[dep] = new HashSet<string>();
graph[dep].Add(toggle.name);
@@ -442,36 +398,29 @@ namespace YOTS }
Dictionary<string, int> inDegree = new Dictionary<string, int>();
- foreach (var toggle in toggleSpecs)
- {
+ foreach (var toggle in toggleSpecs) {
inDegree[toggle.name] = toggle.dependencies.Count;
}
Dictionary<string, int> depths = new Dictionary<string, int>();
Queue<string> queue = new Queue<string>();
- foreach (var pair in inDegree)
- {
- if (pair.Value == 0)
- {
+ foreach (var pair in inDegree) {
+ if (pair.Value == 0) {
queue.Enqueue(pair.Key);
depths[pair.Key] = 0;
}
}
int processedNodes = 0;
- while (queue.Count > 0)
- {
+ while (queue.Count > 0) {
string current = queue.Dequeue();
processedNodes++;
int currentDepth = depths[current];
- if (graph.ContainsKey(current))
- {
- foreach (var neighbor in graph[current])
- {
+ if (graph.ContainsKey(current)) {
+ foreach (var neighbor in graph[current]) {
inDegree[neighbor]--;
- if (inDegree[neighbor] == 0)
- {
+ if (inDegree[neighbor] == 0) {
queue.Enqueue(neighbor);
depths[neighbor] = currentDepth + 1;
}
@@ -479,21 +428,19 @@ namespace YOTS }
}
- if (processedNodes != toggleSpecs.Count)
- {
+ if (processedNodes != toggleSpecs.Count) {
var cycleNodes = toggleSpecs
.Where(t => !depths.ContainsKey(t.name))
.Select(t => t.name)
.ToList();
-
+
throw new System.Exception($"Dependency cycle detected in toggle specifications. Nodes involved: {string.Join(", ", cycleNodes)}");
}
return depths;
}
- private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig(List<ToggleSpec> toggleSpecs)
- {
+ private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig(List<ToggleSpec> toggleSpecs) {
GeneratedAnimatorConfig genAnimatorConfig = new GeneratedAnimatorConfig();
// Sort toggles into layers
Dictionary<string, int> depths = TopologicalSortToggles(toggleSpecs);
@@ -502,33 +449,30 @@ namespace YOTS .OrderBy(g => g.Key)
.ToList();
// Add layers
- for (int i = 0; i < togglesByDepth.Count; i++)
- {
+ for (int i = 0; i < togglesByDepth.Count; i++) {
var depthGroup = togglesByDepth[i];
AnimatorLayer layer = new AnimatorLayer();
layer.name = i == 0 ? "YOTS_BaseLayer" : $"YOTS_OverrideLayer{(i - 1).ToString("00")}";
-
- foreach (var toggle in depthGroup)
- {
+
+ foreach (var toggle in depthGroup) {
string paramName = toggle.name;
if (!genAnimatorConfig.parameters.Any(p => p.name == paramName))
- genAnimatorConfig.parameters.Add(new AnimatorParameterSetting
- {
- name = paramName,
- defaultValue = toggle.defaultValue
+ genAnimatorConfig.parameters.Add(new AnimatorParameterSetting{
+ name = paramName,
+ defaultValue = toggle.defaultValue
});
- layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry {
+ layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggle.name + "_On",
parameter = paramName
});
- layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry {
+ layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggle.name + "_Off",
parameter = paramName
});
}
-
+
genAnimatorConfig.layers.Add(layer);
}
// Add animations
@@ -537,27 +481,21 @@ namespace YOTS return genAnimatorConfig;
}
- private static GeneratedAnimationsConfig GenerateAnimationConfig(List<ToggleSpec> toggleSpecs)
- {
+ private static GeneratedAnimationsConfig GenerateAnimationConfig(List<ToggleSpec> toggleSpecs) {
GeneratedAnimationsConfig genAnimConfig = new GeneratedAnimationsConfig();
- foreach (var toggle in toggleSpecs)
- {
+ foreach (var toggle in toggleSpecs) {
GeneratedAnimationClipConfig onAnim = new GeneratedAnimationClipConfig();
onAnim.name = toggle.name + "_On";
- if (toggle.meshToggles != null)
- {
- foreach (var mesh in toggle.meshToggles)
- {
+ 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,
+ if (toggle.blendShapes != null) {
+ foreach (var bs in toggle.blendShapes) {
+ onAnim.blendShapes.Add(new GeneratedBlendShape{
+ path = bs.path,
+ blendShape = bs.blendShape,
value = bs.onValue
});
}
@@ -566,20 +504,16 @@ namespace YOTS GeneratedAnimationClipConfig offAnim = new GeneratedAnimationClipConfig();
offAnim.name = toggle.name + "_Off";
- if (toggle.meshToggles != null)
- {
- foreach (var mesh in toggle.meshToggles)
- {
+ 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,
+ if (toggle.blendShapes != null) {
+ foreach (var bs in toggle.blendShapes) {
+ offAnim.blendShapes.Add(new GeneratedBlendShape{
+ path = bs.path,
+ blendShape = bs.blendShape,
value = bs.offValue
});
}
@@ -589,16 +523,13 @@ namespace YOTS return genAnimConfig;
}
- private static GeneratedAnimatorConfig ApplyIndependentFixToAnimatorConfig(GeneratedAnimatorConfig genAnimatorConfig)
- {
- float GetOffValueForMesh(string path, List<GeneratedMeshToggle> offList)
- {
+ private static GeneratedAnimatorConfig ApplyIndependentFixToAnimatorConfig(GeneratedAnimatorConfig genAnimatorConfig) {
+ float GetOffValueForMesh(string path, List<GeneratedMeshToggle> offList) {
var offToggle = offList?.FirstOrDefault(mt => mt.path == path);
return offToggle != null ? offToggle.value : 0.0f;
}
- float GetOffValueForBlend(string path, string blendShapeName, List<GeneratedBlendShape> offList)
- {
+ float GetOffValueForBlend(string path, string blendShapeName, List<GeneratedBlendShape> offList) {
var offBlend = offList?.FirstOrDefault(bs => bs.path == path && bs.blendShape == blendShapeName);
return offBlend != null ? offBlend.value : 0.0f;
}
@@ -606,10 +537,8 @@ namespace YOTS Dictionary<string, (GeneratedAnimationClipConfig on, GeneratedAnimationClipConfig off)> toggleAnimations =
new Dictionary<string, (GeneratedAnimationClipConfig, GeneratedAnimationClipConfig)>();
- foreach (var anim in genAnimatorConfig.animations)
- {
- if (anim.name.EndsWith("_On"))
- {
+ foreach (var anim in genAnimatorConfig.animations) {
+ if (anim.name.EndsWith("_On")) {
string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_On"));
if (!toggleAnimations.ContainsKey(toggleName))
toggleAnimations[toggleName] = (null, null);
@@ -617,8 +546,7 @@ namespace YOTS pair.on = anim;
toggleAnimations[toggleName] = pair;
}
- else if (anim.name.EndsWith("_Off"))
- {
+ else if (anim.name.EndsWith("_Off")) {
string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_Off"));
if (!toggleAnimations.ContainsKey(toggleName))
toggleAnimations[toggleName] = (null, null);
@@ -629,11 +557,9 @@ namespace YOTS }
Dictionary<string, int> toggleToLayerIndex = new Dictionary<string, int>();
- for (int i = 0; i < genAnimatorConfig.layers.Count; i++)
- {
+ for (int i = 0; i < genAnimatorConfig.layers.Count; i++) {
var layer = genAnimatorConfig.layers[i];
- foreach (var entry in layer.directBlendTree.entries)
- {
+ foreach (var entry in layer.directBlendTree.entries) {
string entryName = entry.name;
string toggleName = entryName;
if (toggleName.EndsWith("_On"))
@@ -651,34 +577,27 @@ namespace YOTS }
Dictionary<string, HashSet<string>> attributeToToggles = new Dictionary<string, HashSet<string>>();
- foreach (var kvp in toggleAnimations)
- {
+ foreach (var kvp in toggleAnimations) {
string toggleName = kvp.Key;
var pair = kvp.Value;
if (pair.on == null) continue;
HashSet<string> attributes = new HashSet<string>();
- if (pair.on.meshToggles != null)
- {
- foreach (var mt in pair.on.meshToggles)
- {
+ if (pair.on.meshToggles != null) {
+ foreach (var mt in pair.on.meshToggles) {
string attr = "MeshToggle:" + mt.path;
attributes.Add(attr);
}
}
- if (pair.on.blendShapes != null)
- {
- foreach (var bs in pair.on.blendShapes)
- {
+ if (pair.on.blendShapes != null) {
+ foreach (var bs in pair.on.blendShapes) {
string attr = "BlendShape:" + bs.path + "/" + bs.blendShape;
attributes.Add(attr);
}
}
- foreach (var attr in attributes)
- {
- if (!attributeToToggles.TryGetValue(attr, out var set))
- {
+ foreach (var attr in attributes) {
+ if (!attributeToToggles.TryGetValue(attr, out var set)) {
set = new HashSet<string>();
attributeToToggles[attr] = set;
}
@@ -692,27 +611,22 @@ namespace YOTS if (baseLayer == null && genAnimatorConfig.layers.Count > 0)
baseLayer = genAnimatorConfig.layers[0];
- foreach (var kvp in toggleAnimations)
- {
+ foreach (var kvp in toggleAnimations) {
string toggleName = kvp.Key;
var pair = kvp.Value;
int layerIndex = toggleToLayerIndex.ContainsKey(toggleName) ? toggleToLayerIndex[toggleName] : 0;
bool isBase = (layerIndex == 0);
- if (isBase)
- {
+ if (isBase) {
newAnimations.Add(pair.on);
newAnimations.Add(pair.off);
}
- else
- {
+ else {
List<GeneratedMeshToggle> independentMesh = new List<GeneratedMeshToggle>();
List<GeneratedMeshToggle> dependentMesh = new List<GeneratedMeshToggle>();
- if (pair.on.meshToggles != null)
- {
- foreach (var mt in pair.on.meshToggles)
- {
+ if (pair.on.meshToggles != null) {
+ foreach (var mt in pair.on.meshToggles) {
string attr = "MeshToggle:" + mt.path;
if (attributeToToggles[attr].Count == 1)
independentMesh.Add(mt);
@@ -724,10 +638,8 @@ namespace YOTS List<GeneratedBlendShape> independentBlend = new List<GeneratedBlendShape>();
List<GeneratedBlendShape> dependentBlend = new List<GeneratedBlendShape>();
- if (pair.on.blendShapes != null)
- {
- foreach (var bs in pair.on.blendShapes)
- {
+ if (pair.on.blendShapes != null) {
+ foreach (var bs in pair.on.blendShapes) {
string attr = "BlendShape:" + bs.path + "/" + bs.blendShape;
if (attributeToToggles[attr].Count == 1)
independentBlend.Add(bs);
@@ -739,8 +651,7 @@ namespace YOTS bool hasIndependent = (independentMesh.Count > 0 || independentBlend.Count > 0);
bool hasDependent = (dependentMesh.Count > 0 || dependentBlend.Count > 0);
- if (hasIndependent && hasDependent)
- {
+ if (hasIndependent && hasDependent) {
GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
dependentOn.name = toggleName + "_Dependent_On";
dependentOn.meshToggles = dependentMesh;
@@ -749,13 +660,13 @@ namespace YOTS GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig();
dependentOff.name = toggleName + "_Dependent_Off";
dependentOff.meshToggles = dependentMesh
- .Select(mt => new GeneratedMeshToggle {
+ .Select(mt => new GeneratedMeshToggle{
path = mt.path,
value = GetOffValueForMesh(mt.path, pair.off.meshToggles)
})
.ToList();
dependentOff.blendShapes = dependentBlend
- .Select(bs => new GeneratedBlendShape {
+ .Select(bs => new GeneratedBlendShape{
path = bs.path,
blendShape = bs.blendShape,
value = GetOffValueForBlend(bs.path, bs.blendShape, pair.off.blendShapes)
@@ -770,13 +681,13 @@ namespace YOTS GeneratedAnimationClipConfig independentOff = new GeneratedAnimationClipConfig();
independentOff.name = toggleName + "_Independent_Off";
independentOff.meshToggles = independentMesh
- .Select(mt => new GeneratedMeshToggle {
+ .Select(mt => new GeneratedMeshToggle{
path = mt.path,
value = GetOffValueForMesh(mt.path, pair.off.meshToggles)
})
.ToList();
independentOff.blendShapes = independentBlend
- .Select(bs => new GeneratedBlendShape {
+ .Select(bs => new GeneratedBlendShape{
path = bs.path,
blendShape = bs.blendShape,
value = GetOffValueForBlend(bs.path, bs.blendShape, pair.off.blendShapes)
@@ -789,31 +700,25 @@ namespace YOTS newAnimations.Add(independentOff);
AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
- foreach (var entry in overrideLayer.directBlendTree.entries)
- {
+ foreach (var entry in overrideLayer.directBlendTree.entries) {
if (entry.name.StartsWith(toggleName) &&
- (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off")))
- {
+ (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off"))) {
entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
}
}
- if (baseLayer != null)
- {
- baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry
- {
+ if (baseLayer != null) {
+ baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggleName + "_Independent_On",
parameter = toggleName
});
- baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry
- {
+ baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggleName + "_Independent_Off",
parameter = toggleName
});
}
}
- else if (hasIndependent && !hasDependent)
- {
+ else if (hasIndependent && !hasDependent) {
GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig();
independentOn.name = toggleName + "_Independent_On";
independentOn.meshToggles = pair.on.meshToggles;
@@ -828,22 +733,18 @@ namespace YOTS AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
overrideLayer.directBlendTree.entries.RemoveAll(e => e.name.StartsWith(toggleName));
- if (baseLayer != null)
- {
- baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry
- {
+ if (baseLayer != null) {
+ baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggleName + "_Independent_On",
parameter = toggleName
});
- baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry
- {
+ baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
name = toggleName + "_Independent_Off",
parameter = toggleName
});
}
}
- else if (!hasIndependent && hasDependent)
- {
+ else if (!hasIndependent && hasDependent) {
GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
dependentOn.name = toggleName + "_Dependent_On";
dependentOn.meshToggles = pair.on.meshToggles;
@@ -857,11 +758,9 @@ namespace YOTS newAnimations.Add(dependentOff);
AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
- foreach (var entry in overrideLayer.directBlendTree.entries)
- {
+ foreach (var entry in overrideLayer.directBlendTree.entries) {
if (entry.name.StartsWith(toggleName) &&
- (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off")))
- {
+ (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off"))) {
entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
}
}
@@ -873,21 +772,17 @@ namespace YOTS return genAnimatorConfig;
}
- private static GeneratedAnimatorConfig RemoveOffAnimationsFromOverrideLayers(GeneratedAnimatorConfig config)
- {
- for (int i = 1; i < config.layers.Count; i++)
- {
+ private static GeneratedAnimatorConfig RemoveOffAnimationsFromOverrideLayers(GeneratedAnimatorConfig config) {
+ for (int i = 1; i < config.layers.Count; i++) {
var layer = config.layers[i];
layer.directBlendTree.entries.RemoveAll(entry => entry.name.EndsWith("_Off"));
}
return config;
}
- private static GeneratedAnimatorConfig RemoveUnusedAnimations(GeneratedAnimatorConfig config)
- {
+ private static GeneratedAnimatorConfig RemoveUnusedAnimations(GeneratedAnimatorConfig config) {
HashSet<string> referencedAnimations = new HashSet<string>();
- foreach (var layer in config.layers)
- {
+ foreach (var layer in config.layers) {
foreach (var entry in layer.directBlendTree.entries)
referencedAnimations.Add(entry.name);
}
@@ -900,19 +795,16 @@ namespace YOTS }
private static VRCExpressionsMenu GetOrCreateSubmenu(
- VRCExpressionsMenu parentMenu,
+ VRCExpressionsMenu parentMenu,
string submenuName
- )
- {
+ ) {
if (parentMenu.controls == null)
parentMenu.controls = new List<VRCExpressionsMenu.Control>();
// Check if submenu already exists
- foreach (var control in parentMenu.controls)
- {
+ foreach (var control in parentMenu.controls) {
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
- control.name == submenuName && control.subMenu != null)
- {
+ control.name == submenuName && control.subMenu != null) {
// Clone existing submenu to avoid modifying original
var clonedSubmenu = UnityEngine.Object.Instantiate(control.subMenu);
control.subMenu = clonedSubmenu;
@@ -925,8 +817,7 @@ namespace YOTS newSubmenu.name = submenuName;
newSubmenu.controls = new List<VRCExpressionsMenu.Control>();
- var newControl = new VRCExpressionsMenu.Control
- {
+ var newControl = new VRCExpressionsMenu.Control{
name = submenuName,
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
subMenu = newSubmenu
@@ -936,31 +827,26 @@ namespace YOTS return newSubmenu;
}
- private static void InitializeSubmenu(VRCExpressionsMenu menu)
- {
+ private static void InitializeSubmenu(VRCExpressionsMenu menu) {
if (menu == null) return;
-
- if (menu.controls != null)
- {
- foreach (var control in menu.controls)
- {
+
+ if (menu.controls != null) {
+ foreach (var control in menu.controls) {
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null)
InitializeSubmenu(control.subMenu);
}
menu.controls.Clear();
}
- else
- {
+ else {
menu.controls = new List<VRCExpressionsMenu.Control>();
}
}
private static void GenerateVRChatAssets(
- List<ToggleSpec> toggleSpecs,
+ List<ToggleSpec> toggleSpecs,
VRCExpressionParameters vrcParams,
VRCExpressionsMenu vrcMenu
- )
- {
+ ) {
var uniqueToggles = toggleSpecs
.Where(t => t.name != "YOTS_Weight")
.GroupBy(t => t.name)
@@ -969,10 +855,8 @@ namespace YOTS var paramList = new List<VRCExpressionParameters.Parameter>();
paramList.AddRange(vrcParams.parameters.Where(p => !uniqueToggles.Any(t => t.name == p.name)));
- foreach (var toggle in uniqueToggles)
- {
- paramList.Add(new VRCExpressionParameters.Parameter
- {
+ foreach (var toggle in uniqueToggles) {
+ paramList.Add(new VRCExpressionParameters.Parameter{
name = toggle.name,
valueType = toggle.type == "radial" ? VRCExpressionParameters.ValueType.Float : VRCExpressionParameters.ValueType.Bool,
defaultValue = toggle.defaultValue,
@@ -987,35 +871,28 @@ namespace YOTS yotsSubmenu.controls = new List<VRCExpressionsMenu.Control>();
// Track all created/modified menus to ensure they're saved
HashSet<VRCExpressionsMenu> modifiedMenus = new HashSet<VRCExpressionsMenu> { vrcMenu, yotsSubmenu };
- foreach (var toggle in toggleSpecs)
- {
+ foreach (var toggle in toggleSpecs) {
VRCExpressionsMenu currentMenu = yotsSubmenu;
- if (!string.IsNullOrEmpty(toggle.menuPath) && toggle.menuPath != "/")
- {
+ if (!string.IsNullOrEmpty(toggle.menuPath) && toggle.menuPath != "/") {
string trimmedPath = toggle.menuPath.Trim('/');
var sections = trimmedPath.Split('/');
- foreach (var section in sections)
- {
+ foreach (var section in sections) {
currentMenu = GetOrCreateSubmenu(currentMenu, section);
modifiedMenus.Add(currentMenu);
}
}
// Add toggle controls
- if (toggle.type == "radial")
- {
- currentMenu.controls.Add(new VRCExpressionsMenu.Control
- {
+ if (toggle.type == "radial") {
+ currentMenu.controls.Add(new VRCExpressionsMenu.Control{
name = toggle.name,
type = VRCExpressionsMenu.Control.ControlType.RadialPuppet,
- subParameters = new VRCExpressionsMenu.Control.Parameter[] {
+ subParameters = new VRCExpressionsMenu.Control.Parameter[]{
new VRCExpressionsMenu.Control.Parameter { name = toggle.name }
}
});
}
- else
- {
- currentMenu.controls.Add(new VRCExpressionsMenu.Control
- {
+ else {
+ currentMenu.controls.Add(new VRCExpressionsMenu.Control{
name = toggle.name,
type = VRCExpressionsMenu.Control.ControlType.Toggle,
parameter = new VRCExpressionsMenu.Control.Parameter { name = toggle.name },
@@ -1025,8 +902,7 @@ namespace YOTS }
// Add YOTS submenu to main menu
- vrcMenu.controls.Add(new VRCExpressionsMenu.Control
- {
+ vrcMenu.controls.Add(new VRCExpressionsMenu.Control{
name = "YOTS",
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
subMenu = yotsSubmenu
diff --git a/Scripts/YOTSNDMFConfig.cs b/Scripts/YOTSNDMFConfig.cs index 0609d18..9b2e71e 100644 --- a/Scripts/YOTSNDMFConfig.cs +++ b/Scripts/YOTSNDMFConfig.cs @@ -7,13 +7,11 @@ using VRC.SDK3.Avatars.ScriptableObjects; namespace YOTS
{
[DisallowMultipleComponent]
- public class YOTSNDMFConfig : MonoBehaviour
- {
+ public class YOTSNDMFConfig : MonoBehaviour {
[Tooltip("The JSON configuration file.")]
public TextAsset jsonConfig;
- void OnValidate()
- {
+ void OnValidate() {
gameObject.tag = "EditorOnly";
}
}
diff --git a/Scripts/YOTSNDMFGenerator.cs b/Scripts/YOTSNDMFGenerator.cs index c3d4054..525953c 100644 --- a/Scripts/YOTSNDMFGenerator.cs +++ b/Scripts/YOTSNDMFGenerator.cs @@ -16,8 +16,7 @@ using UnityEditor.Animations; namespace YOTS
{
- public class YOTSNDMFGenerator : Plugin<YOTSNDMFGenerator>
- {
+ public class YOTSNDMFGenerator : Plugin<YOTSNDMFGenerator> {
private readonly Localizer localizer = new Localizer("en-us", () =>
new List<(string, Func<string, string>)> {
("en-us", key => key)
@@ -25,8 +24,7 @@ namespace YOTS public override string DisplayName => "YOTS Animator Generator";
- protected override void Configure()
- {
+ protected override void Configure() {
// First pass: Retrieve and stash configuration
InPhase(BuildPhase.Resolving)
.Run("Cache YOTS Config", ctx => {
@@ -82,8 +80,7 @@ namespace YOTS RuntimeAnimatorController originalAnimator = descriptor.baseAnimationLayers[4].animatorController;
var menu = descriptor.expressionsMenu;
var parameters = descriptor.expressionParameters;
- if (parameters == null || menu == null)
- {
+ if (parameters == null || menu == null) {
ErrorReport.WithContextObject(descriptor, () => {
ErrorReport.ReportException(
new Exception("Missing required VRC assets"),
@@ -115,8 +112,7 @@ namespace YOTS }
// If no original animator, just assign the generated one.
- if (originalAnimator == null)
- {
+ if (originalAnimator == null) {
descriptor.baseAnimationLayers[4].animatorController = generatedAnimator;
return;
}
@@ -129,14 +125,11 @@ namespace YOTS }
// Simply append generated params and layers to the original animator.
- private static void MergeAnimatorControllers(Localizer localizer, AnimatorController original, AnimatorController generated)
- {
+ private static void MergeAnimatorControllers(Localizer localizer, AnimatorController original, AnimatorController generated) {
// Merge parameters from generated into original.
- foreach (var genParam in generated.parameters)
- {
+ foreach (var genParam in generated.parameters) {
// This is an O(m*n) check but m and n should be small enough to not matter.
- if (original.parameters.Any(p => p.name == genParam.name))
- {
+ if (original.parameters.Any(p => p.name == genParam.name)) {
ErrorReport.WithContextObject(original, () => {
ErrorReport.ReportException(
new Exception($"Parameter '{genParam.name}' already exists"),
@@ -149,12 +142,10 @@ namespace YOTS }
// Append each YOTS layer after the original layers.
- foreach (var genLayer in generated.layers)
- {
+ foreach (var genLayer in generated.layers) {
// This isn't strictly an error but if someone already has layers named
// YOTS_* that's probably not on purpose.
- if (original.layers.Any(l => l.name == genLayer.name))
- {
+ if (original.layers.Any(l => l.name == genLayer.name)) {
ErrorReport.WithContextObject(original, () => {
ErrorReport.ReportException(
new Exception($"Layer '{genLayer.name}' already exists"),
@@ -163,8 +154,7 @@ namespace YOTS });
return;
}
- var newLayer = new AnimatorControllerLayer
- {
+ var newLayer = new AnimatorControllerLayer {
name = genLayer.name,
defaultWeight = genLayer.defaultWeight,
stateMachine = genLayer.stateMachine
@@ -173,8 +163,7 @@ namespace YOTS }
}
- private class YOTSBuildState
- {
+ private class YOTSBuildState {
public string jsonConfig;
}
}
|
