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