yum-slop/YOTS

an optimized toggle system for vrchat

git clone https://git.yummers.dev/yum-slop/YOTS

yumName resolution now only works for gameobjects with a meshrenderer or skinned mesh renderera72d211

master
11.5 KiB280 linesraw
1#if UNITY_EDITOR
2
3using UnityEngine;
4using nadena.dev.ndmf;
5using nadena.dev.ndmf.builtin;
6using nadena.dev.ndmf.localization;
7using nadena.dev.ndmf.VRChat;
8using VRC.SDK3.Avatars.ScriptableObjects;
9using UnityEditor;
10using System;
11using System.Collections.Generic;
12using System.Linq;
13using UnityEditor.Animations;
14
15[assembly: ExportsPlugin(typeof(YOTS.YOTSNDMFGenerator))]
16
17namespace YOTS
18{
19  public class YOTSNDMFGenerator : Plugin<YOTSNDMFGenerator> {
20    private readonly Localizer lcl = new Localizer("en-us", () =>
21        new List<(string, Func<string, string>)> {
22        ("en-us", key => {
23          switch (key) {
24            case "json_missing": return "YOTS configuration JSON file is missing";
25            case "config_missing": return "YOTS config component not found on avatar";
26            case "descriptor_missing": return "VRC Avatar Descriptor is missing from avatar";
27            case "expressions_missing": return "Avatar is missing Expression Parameters or Expression Menu";
28            case "param_exists": return "Parameter '{0}' already exists in FX animator";
29            case "layer_exists": return "Layer '{0}' already exists in FX animator";
30            case "config_error": return "{0}";
31            default: return null;
32          }
33        })
34        });
35
36    public override string DisplayName => "YOTS Animator Generator";
37
38    protected override void Configure() {
39      // First pass: Retrieve and stash configuration. By the time we're in the
40      // Transforming phase, we can no longer access the YOTSConfig object.
41      InPhase(BuildPhase.Resolving)
42        .Run("Cache YOTS Config", ctx => {
43          var configs = ctx.AvatarRootObject.GetComponentsInChildren<YOTSConfig>();
44          if (configs.Length == 0) {
45            ctx.GetState<YOTSBuildState>().skipGeneration = true;
46            Debug.Log("No YOTS config found - skipping.");
47            return;
48          }
49          var mergedConfig = new AnimatorConfigFile();
50          foreach (var config in configs) {
51            if (config.jsonConfig == null) {
52              ctx.GetState<YOTSBuildState>().skipGeneration = true;
53              ErrorReport.ReportError(lcl, ErrorSeverity.Error, "json_missing",
54                  config.gameObject);
55              return;
56            }
57            var parsed = JsonUtility.FromJson<AnimatorConfigFile>(config.jsonConfig.text);
58            mergedConfig.objectSets.AddRange(parsed.objectSets);
59            mergedConfig.toggles.AddRange(parsed.toggles);
60            if (parsed.api_version != null) {
61              mergedConfig.api_version = parsed.api_version;
62            }
63          }
64          ctx.GetState<YOTSBuildState>().jsonConfig = JsonUtility.ToJson(mergedConfig);
65        })
66        // Shoutsout anatawa12/AvatarOptimizer
67        .BeforePass(RemoveEditorOnlyPass.Instance);
68
69      // Second pass: Generate and merge animator
70      InPhase(BuildPhase.Transforming)
71        .Run("Generate YOTS Animator", ctx => {
72          var config = ctx.GetState<YOTSBuildState>();
73          if (config.skipGeneration) {
74            return;
75          }
76          if (config == null) {
77            ErrorReport.ReportError(lcl, ErrorSeverity.Error, "config_missing",
78                ctx.AvatarRootObject);
79            return;
80          }
81          if (config.jsonConfig == null) {
82            ErrorReport.ReportError(lcl, ErrorSeverity.Error, "json_missing",
83                ctx.AvatarRootObject);
84            return;
85          }
86
87          // Get menu and parameters
88          var descriptor = ctx.AvatarDescriptor;
89          if (descriptor == null) {
90            ErrorReport.ReportError(lcl, ErrorSeverity.Error, "descriptor_missing",
91                ctx.AvatarRootObject);
92            return;
93          }
94          RuntimeAnimatorController originalAnimator = descriptor.baseAnimationLayers[4].animatorController;
95          var menu = descriptor.expressionsMenu;
96          var parameters = descriptor.expressionParameters;
97          if (parameters == null || menu == null) {
98            ErrorReport.ReportError(lcl, ErrorSeverity.Error, "expressions_missing",
99                descriptor);
100            return;
101          }
102          // Create copies so the originals don't get modified
103          menu = DeepCopyMenu(menu);
104          parameters = UnityEngine.Object.Instantiate(parameters);
105          descriptor.expressionsMenu = menu;
106          descriptor.expressionParameters = parameters;
107
108          // Resolve bare names to full hierarchy paths.
109          var resolvedJson = ResolveNames(config.jsonConfig, ctx.AvatarRootObject.transform);
110
111          // Generate the YOTS animator.
112          RuntimeAnimatorController generatedAnimator = null;
113          try {
114            generatedAnimator = YOTSCore.GenerateAnimator(
115                resolvedJson,
116                parameters,
117                menu
118            );
119          } catch (ArgumentException e) {
120            ErrorReport.ReportError(lcl, ErrorSeverity.Error, "config_error",
121                e.Message, ctx.AvatarRootObject);
122            return;
123          } catch (Exception e) {
124            ErrorReport.ReportException(e);
125            return;
126          }
127
128          // If no original animator, just assign the generated one.
129          if (originalAnimator == null) {
130            descriptor.baseAnimationLayers[4].animatorController = generatedAnimator;
131            return;
132          }
133          // Else append the generated animator to the original.
134          AnimatorController originalController = originalAnimator as AnimatorController;
135          AnimatorController generatedController = generatedAnimator as AnimatorController;
136          MergeAnimatorControllers(originalController, generatedController);
137          descriptor.baseAnimationLayers[4].animatorController = generatedController;
138        });
139    }
140
141    // Simply append generated params and layers to the original animator.
142    private void MergeAnimatorControllers(AnimatorController from, AnimatorController to) {
143      // Merge parameters from from into to.
144      foreach (var genParam in from.parameters) {
145        // This is an O(m*n) check but m and n should be small enough to not matter.
146        if (to.parameters.Any(p => p.name == genParam.name)) {
147          ErrorReport.ReportError(lcl, ErrorSeverity.Error,
148              "param_exists",
149              genParam.name, to);
150          return;
151        }
152        to.AddParameter(genParam);
153      }
154
155      // Append each YOTS layer after the to layers.
156      foreach (var genLayer in from.layers) {
157        // This isn't strictly an error but if someone already has layers named
158        // YOTS_* that's probably not on purpose.
159        if (to.layers.Any(l => l.name == genLayer.name)) {
160          ErrorReport.ReportError(lcl, ErrorSeverity.Error,
161              "layer_exists",
162              genLayer.name, to);
163          return;
164        }
165        var newLayer = new AnimatorControllerLayer {
166          name = genLayer.name,
167          defaultWeight = genLayer.defaultWeight,
168          stateMachine = genLayer.stateMachine
169        };
170        to.AddLayer(newLayer);
171      }
172    }
173
174    private class YOTSBuildState {
175      public string jsonConfig;
176      public bool skipGeneration;
177    }
178
179    // Resolve bare names (no '/') to full hierarchy paths. Names containing
180    // '/' are kept as explicit paths. Also expands object set references into paths.
181    private static string ResolveNames(string jsonConfig, Transform avatarRoot) {
182      var config = JsonUtility.FromJson<AnimatorConfigFile>(jsonConfig);
183
184      // Build set lookup.
185      var setMap = new Dictionary<string, List<string>>();
186      if (config.objectSets != null) {
187        foreach (var set in config.objectSets) {
188          setMap[set.name] = set.objects;
189        }
190      }
191
192      // Expand set references into paths, then resolve bare names.
193      var nameToPathsMap = BuildNameToPathsMap(avatarRoot);
194      foreach (var toggle in config.toggles) {
195        toggle.meshToggles = ExpandNames(toggle.meshToggles, nameToPathsMap);
196        toggle.inverseMeshToggles = ExpandNames(toggle.inverseMeshToggles, nameToPathsMap);
197        foreach (var bs in toggle.blendShapes) {
198          bs.paths = ExpandSets(bs.paths, bs.sets, setMap, toggle.name, "blendShape");
199          bs.path = ExpandName(bs.path, nameToPathsMap);
200          bs.paths = ExpandNames(bs.paths, nameToPathsMap);
201        }
202        foreach (var st in toggle.shaderToggles) {
203          st.paths = ExpandSets(st.paths, st.sets, setMap, toggle.name, "shaderToggle");
204          st.path = ExpandName(st.path, nameToPathsMap);
205          st.paths = ExpandNames(st.paths, nameToPathsMap);
206        }
207      }
208      return JsonUtility.ToJson(config);
209    }
210
211    // Expand object set references into the paths list.
212    private static List<string> ExpandSets(List<string> paths, List<string> sets,
213        Dictionary<string, List<string>> setMap, string toggleName, string specType) {
214      if (sets == null || sets.Count == 0) return paths;
215      var result = paths != null ? new List<string>(paths) : new List<string>();
216      foreach (var setName in sets) {
217        if (setMap.TryGetValue(setName, out var objects)) {
218          result.AddRange(objects);
219        } else {
220          throw new ArgumentException(
221            $"{specType} in '{toggleName}' references unknown object set '{setName}'");
222        }
223      }
224      return result;
225    }
226
227    private static string ExpandName(string name, Dictionary<string, List<string>> nameToPathsMap) {
228      if (string.IsNullOrEmpty(name) || name.Contains('/')) return name;
229      if (nameToPathsMap.TryGetValue(name, out var paths) && paths.Count == 1) return paths[0];
230      return name;
231    }
232
233    private static List<string> ExpandNames(List<string> names, Dictionary<string, List<string>> nameToPathsMap) {
234      if (names == null) return null;
235      var resolved = new List<string>();
236      foreach (var name in names) {
237        if (name.Contains('/')) {
238          resolved.Add(name);
239        } else if (nameToPathsMap.TryGetValue(name, out var paths)) {
240          resolved.AddRange(paths);
241        } else {
242          resolved.Add(name);
243        }
244      }
245      return resolved;
246    }
247
248    private static Dictionary<string, List<string>> BuildNameToPathsMap(Transform root) {
249      var map = new Dictionary<string, List<string>>();
250      CollectPaths(root, "", map);
251      return map;
252    }
253
254    private static void CollectPaths(Transform current, string currentPath, Dictionary<string, List<string>> map) {
255      foreach (Transform child in current) {
256        string childPath = currentPath == "" ? child.name : currentPath + "/" + child.name;
257        if (child.GetComponent<SkinnedMeshRenderer>() != null || child.GetComponent<MeshRenderer>() != null) {
258          if (!map.ContainsKey(child.name))
259            map[child.name] = new List<string>();
260          map[child.name].Add(childPath);
261        }
262        CollectPaths(child, childPath, map);
263      }
264    }
265
266    private static VRCExpressionsMenu DeepCopyMenu(VRCExpressionsMenu sourceMenu) {
267        var copiedMenu = UnityEngine.Object.Instantiate(sourceMenu);
268        // Deep copy all submenu references
269        for (int i = 0; i < copiedMenu.controls.Count; i++) {
270            var control = copiedMenu.controls[i];
271            if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) {
272                control.subMenu = DeepCopyMenu(control.subMenu);
273            }
274        }
275        return copiedMenu;
276    }
277  }
278}
279
280#endif  // UNITY_EDITOR