yum-slop/YOTS
an optimized toggle system for vrchat
git clone https://git.yummers.dev/yum-slop/YOTS
161ccba
master
1#ifUNITY_EDITOR 2 3using System ; 4using System . Collections . Generic ; 5using System . IO ; 6using System . Linq ; 7using System . Text . RegularExpressions ; 8using UnityEngine ; 9using UnityEditor ; 10using UnityEditor . Animations ; 11using VRC . SDK3 . Avatars . Components ; 12using VRC . SDK3 . Avatars . ScriptableObjects ; 13 14namespace YOTS 15{ 16[ System . Serializable ] 17public class ToggleSpec { 18// The name of the toggle. This is shown in the menu. 19[ SerializeField ] 20public string name ; 21 22// The type of toggle. 23// Accepted values: 24// "toggle" - A boolean toggle. Creates a boolean sync param. 25// "radial" - A radial puppet. Creates a float sync param. 26[ SerializeField ] 27public string type = "toggle" ; 28 29// The name of the parameter to use. 30// If not specified, the name will be generated from the menuPath and name. 31[ SerializeField ] 32public string parameterName ; 33 34// Dependencies are toggles that will be evaluated before this one. If 35// you have two toggles which animate the same thing, one must depend 36// on the other. 37[ SerializeField ] 38public List < string > dependencies = new List < string > (); 39 40// The name of meshes to toggle. 41// For example, "Body" or "Shirt". 42[ SerializeField ] 43public List < string > meshToggles = new List < string > (); 44 45// The name of meshes to *disable* when the toggle is turned on. 46// For example, if you want to hide certain meshes when this toggle is active. 47[ SerializeField ] 48public List < string > inverseMeshToggles = new List < string > (); 49 50// Blendshapes to animate. 51[ SerializeField ] 52public List < BlendShapeSpec > blendShapes = new List < BlendShapeSpec > (); 53 54// Material properties to animate. 55[ SerializeField ] 56public List < ShaderToggleSpec > shaderToggles = new List < ShaderToggleSpec > (); 57 58// External animations to use. 59[ SerializeField ] 60public List < ExternalAnimationSpec > externalAnimations = new List < ExternalAnimationSpec > (); 61 62// Where to put the toggle in the menu. Defaults to the top-level menu. 63// For example, if you put "Clothes" here, it'll be placed under /Clothes. 64[ SerializeField ] 65public string menuPath = "/" ; 66 67// The default value of the toggle. Range from 0-1. 68// For example, if you want a gimmick to start toggled off, set this to 69// 0.0f. 70[ SerializeField ] 71public float defaultValue = 1.0f ; 72 73// Fully drive the "off" animation when the parameter is below this threshold. 74[ SerializeField ] 75public float offThreshold = 0.0f ; 76 77// Fully drive the "on" animation when the parameter is above this threshold. 78[ SerializeField ] 79public float onThreshold = 1.0f ; 80 81// Default value for blendshapes when the toggle is off. Range from 0-100. 82// Individual blendshapes can override this with their own offValue. 83[ SerializeField ] 84public float offValue = 0.0f ; 85 86// Default value for blendshapes when the toggle is on. Range from 0-100. 87// Individual blendshapes can override this with their own onValue. 88[ SerializeField ] 89public float onValue = 100.0f ; 90 91// Whether the corresponding VRChat parameter is synced. 92[ SerializeField ] 93public bool synced = true ; 94 95// Whether the corresponding VRChat parameter is saved. 96[ SerializeField ] 97public bool saved = true ; 98 99// If true, no menu entry will be created for this toggle. 100// The parameter will still be created and can be controlled by other means 101// - for example, via contacts. 102[ SerializeField ] 103public bool disableMenuEntry = false ; 104 105// If true, it's as if this ToggleSpec doesn't exist. 106[ SerializeField ] 107public bool disabled = false ; 108 109// Parent constraint weights to animate 110[ SerializeField ] 111public List < ParentConstraintWeight > parentConstraintWeights = new List < ParentConstraintWeight > (); 112 113// Get the effective parameter name, generating one if not specified 114public string GetParameterName () { 115// Use explicit parameter name if provided 116if ( ! string . IsNullOrEmpty ( parameterName )) { 117return parameterName ; 118} 119 120// Otherwise, generate one based on menu structure 121if ( disableMenuEntry ) { 122return name ; 123} 124 125return menuPath . TrimEnd ( '/' ) + "/" + name ; 126} 127} 128 129[ System . Serializable ] 130public class BlendShapeSpec { 131// The name of the blend shape to apply. 132// For example, "Chest_Hide" or "Boobs+". 133[ SerializeField ] 134public string blendShape ; 135 136public List < string > blendShapes = new List < string > (); 137 138// The path to the mesh renderer to apply the blend shape to. 139// For example, "Body" or "Shirt". 140[ SerializeField ] 141public string path ; 142 143[ SerializeField ] 144public List < string > paths = new List < string > (); 145 146// Names of object sets to use as paths. 147[ SerializeField ] 148public List < string > sets = new List < string > (); 149 150// The value of the blendshape when the toggle is off. Range from 0-100. 151[ SerializeField ] 152public float offValue = 0.0f ; 153 154// The value of the blendshape when the toggle is on. Range from 0-100. 155[ SerializeField ] 156public float onValue = 100.0f ; 157} 158 159[ System . Serializable ] 160public class ShaderToggleSpec { 161[ SerializeField ] 162public string materialProperty ; 163 164[ SerializeField ] 165public string path = "" ; 166 167[ SerializeField ] 168public List < string > paths = new List < string > (); 169 170// Names of object sets to use as paths. 171[ SerializeField ] 172public List < string > sets = new List < string > (); 173 174[ SerializeField ] 175public float offValue = 0.0f ; 176 177[ SerializeField ] 178public float onValue = 1.0f ; 179 180[ SerializeField ] 181public string rendererType = "SkinnedMeshRenderer" ; // Can be "SkinnedMeshRenderer" or "MeshRenderer" 182} 183 184[ System . Serializable ] 185public class ParentConstraintWeight { 186[ SerializeField ] 187public string path = "" ; 188 189[ SerializeField ] 190public float offValue = 0.0f ; 191 192[ SerializeField ] 193public float onValue = 1.0f ; 194} 195 196[ System . Serializable ] 197public class ObjectSet { 198[ SerializeField ] 199public string name ; 200 201[ SerializeField ] 202public List < string > objects = new List < string > (); 203} 204 205[ System . Serializable ] 206public class AnimatorConfigFile { 207[ SerializeField ] 208public List < ObjectSet > objectSets = new List < ObjectSet > (); 209 210[ SerializeField ] 211public List < ToggleSpec > toggles = new List < ToggleSpec > (); 212 213[ SerializeField ] 214public string api_version ; 215} 216 217[ System . Serializable ] 218public class GeneratedAnimationsConfig { 219public List < GeneratedAnimationClipConfig > animations = 220new List < GeneratedAnimationClipConfig > (); 221} 222 223[ System . Serializable ] 224public class GeneratedAnimationClipConfig { 225public string name ; 226public List < GeneratedMeshToggle > meshToggles = 227new List < GeneratedMeshToggle > (); 228public List < GeneratedBlendShape > blendShapes = 229new List < GeneratedBlendShape > (); 230public List < GeneratedShaderToggle > shaderToggles = 231new List < GeneratedShaderToggle > (); 232public List < GeneratedParentConstraint > parentConstraintWeights = new List < GeneratedParentConstraint > (); 233} 234 235[ System . Serializable ] 236public class GeneratedMeshToggle { 237public string path ; 238public float value ; 239} 240 241[ System . Serializable ] 242public class GeneratedBlendShape { 243public string path ; 244public string blendShape ; 245public float value ; 246} 247 248// Add new class for generated shader toggles 249[ System . Serializable ] 250public class GeneratedShaderToggle { 251public string path ; 252public string materialProperty ; 253public float value ; 254public string rendererType = "SkinnedMeshRenderer" ; // Default to SkinnedMeshRenderer for backward compatibility 255} 256 257// Add new class for generated parent constraints 258[ System . Serializable ] 259public class GeneratedParentConstraint { 260public string path ; 261public float value ; 262} 263 264[ System . Serializable ] 265public class ExternalAnimationSpec { 266// Path to the "on" animation clip asset (e.g., "Assets/MyAnims/External_On.anim") 267[ SerializeField ] 268public string onClipPath ; 269// Path to the "off" animation clip asset (e.g., "Assets/MyAnims/External_Off.anim") 270[ SerializeField ] 271public string offClipPath ; 272[ SerializeField ] 273public bool mirror = false ; 274} 275 276// These classes describe the generated JSON output for the animator configuration. 277[ System . Serializable ] 278public class GeneratedAnimatorConfig { 279public List < AnimatorParameterSetting > parameters = new List < AnimatorParameterSetting > (); 280public List < AnimatorLayer > layers = new List < AnimatorLayer > (); 281public List < GeneratedAnimationClipConfig > animations = 282new List < GeneratedAnimationClipConfig > (); 283} 284 285[ System . Serializable ] 286public class AnimatorLayer { 287public string name ; 288public AnimatorDirectBlendTree directBlendTree = 289new AnimatorDirectBlendTree (); 290} 291 292[ System . Serializable ] 293public class AnimatorDirectBlendTree { 294public List < AnimatorDirectBlendTreeEntry > entries = 295new List < AnimatorDirectBlendTreeEntry > (); 296} 297 298[ System . Serializable ] 299public class AnimatorDirectBlendTreeEntry { 300public string name ; // animation name 301public string parameter ; // parameter driving the animation 302public float offThreshold = 0.0f ; // threshold for off animation 303public float onThreshold = 1.0f ; // threshold for on animation 304} 305 306// Add these new classes at the namespace level 307[ System . Serializable ] 308public class VRCMenuConfig { 309public string menuName = "YOTS" ; 310public List < VRCMenuItemConfig > items = new List < VRCMenuItemConfig > (); 311} 312 313[ System . Serializable ] 314public class VRCMenuItemConfig { 315public string name ; 316public string parameter ; 317public Texture2D icon ; 318} 319 320[ System . Serializable ] 321public class AnimatorParameterSetting { 322public string name ; 323public float defaultValue ; 324public float offThreshold = 0.0f ; 325public float onThreshold = 1.0f ; 326} 327 328public class YOTSCore { 329private static Dictionary < string , AnimationClip > animationClips = new Dictionary < string , AnimationClip > (); 330 331private static string GetMeshToggleAttributeId ( string path ) { 332return "MeshToggle:" + path ; 333} 334 335private static string GetBlendShapeAttributeId ( string path , string blendShape ) { 336return "BlendShape:" + path + "/" + blendShape ; 337} 338 339private static string GetShaderToggleAttributeId ( string path , string materialProperty ) { 340return "ShaderToggle:" + path + "/" + materialProperty ; 341} 342 343private static string GetParentConstraintAttributeId ( string path ) { 344return "ParentConstraint:" + path ; 345} 346 347public static AnimatorController GenerateAnimator ( string configJson , 348VRCExpressionParameters vrcParams , VRCExpressionsMenu vrcMenu ) { 349Debug . Log ( "=== Starting Animator Generation Process ===" ); 350 351if ( string . IsNullOrEmpty ( configJson )) { 352throw new ArgumentException ( "No config JSON provided." ); 353} 354 355AnimatorConfigFile config ; 356config = JsonUtility . FromJson < AnimatorConfigFile > ( configJson ); 357if ( config == null ) { 358throw new ArgumentException ( "JSON config is empty or invalid" ); 359} 360 361// Remove disabled specs upon ingest so that the usual checks apply. 362int n_removed = config . toggles . RemoveAll ( spec=> spec . disabled ); 363Debug . Log ( $"Removed { n_removed } disabled toggles" ); 364 365if ( config . toggles == null ) { 366throw new ArgumentException ( "No toggleSpecs found in configuration" ); 367} 368Debug . Log ( $"Configuration loaded. Found { config . toggles . Count } toggles." ); 369 370// Create abstract representation of the animator structure. 371GeneratedAnimatorConfig genAnimatorConfig = GenerateNaiveAnimatorConfig ( config . toggles ); 372genAnimatorConfig = ApplyIndependentFixToAnimatorConfig ( genAnimatorConfig ); 373genAnimatorConfig = RemoveOffAnimationsFromOverrideLayers ( genAnimatorConfig ); 374genAnimatorConfig = RemoveUnusedAnimations ( genAnimatorConfig ); 375 376animationClips . Clear (); 377Debug . Log ( "--- Preparing Final Animation Clips ---" ); 378 379// Create lookup from animation name to toggle spec 380Dictionary < string , ToggleSpec > animNameToToggleSpec = new Dictionary < string , ToggleSpec > (); 381foreach ( var toggle in config . toggles ) { 382string paramName = toggle . GetParameterName (); 383string animName = paramName ; 384if ( config . toggles . Count ( t=> t . GetParameterName () == paramName ) > 1 ) { 385animName = paramName + "_" + toggle . name ; 386} 387animNameToToggleSpec [ animName ] = toggle ; 388} 389 390// Iterate through the FINAL animation configurations after potential renaming/splitting 391foreach ( var finalAnimConfig in genAnimatorConfig . animations ) { 392string finalClipName = finalAnimConfig . name ; 393 394// Determine the original base animation name from the final clip name 395string baseAnimName = finalClipName ; 396string [] suffixes = { "_Independent_On" , "_Independent_Off" , "_Dependent_On" , "_Dependent_Off" , "_On" , "_Off" }; 397foreach ( var suffix in suffixes ) { 398if ( baseAnimName . EndsWith ( suffix )) { 399baseAnimName = baseAnimName . Substring ( 0 , baseAnimName . Length - suffix . Length ); 400break ; 401} 402} 403 404if ( ! animNameToToggleSpec . TryGetValue ( baseAnimName , out ToggleSpec originalToggleSpec )) { 405Debug . LogError ( $"Could not find original ToggleSpec for animation name ' { baseAnimName } ' derived from animation clip ' { finalClipName } '. Skipping clip." ); 406continue ; 407} 408 409bool usesExternal = originalToggleSpec . externalAnimations != null && originalToggleSpec . externalAnimations . Count > 0 ; 410 411if ( usesExternal ) { 412var externalSpec = originalToggleSpec . externalAnimations [ 0 ]; 413string sourceClipPath = null ; 414bool isOffClip = finalClipName . EndsWith ( "_Off" ) || finalClipName . EndsWith ( "_Independent_Off" ) || finalClipName . EndsWith ( "_Dependent_Off" ); 415 416sourceClipPath = isOffClip ? externalSpec . offClipPath : externalSpec . onClipPath ; 417 418if ( string . IsNullOrEmpty ( sourceClipPath )) { 419Debug . LogError ( $"Toggle ' { originalToggleSpec . name } ' (Param: ' { originalToggleSpec . GetParameterName ()} '): External clip path is missing for ' { finalClipName } '. Skipping clip." ); 420continue ; 421} 422 423AnimationClip sourceClip = AssetDatabase . LoadAssetAtPath < AnimationClip > ( sourceClipPath ); 424if ( sourceClip == null ) { 425Debug . LogError ( $"Toggle ' { originalToggleSpec . name } ' (Param: ' { originalToggleSpec . GetParameterName ()} '): Failed to load source external animation clip ' { finalClipName } ' at path: { sourceClipPath } . Skipping clip." ); 426continue ; 427} 428 429AnimationClip clipToUse = null ; 430 431if ( externalSpec . mirror ) { 432// Generate mirrored clip in memory 433Debug . Log ( $"Generating in-memory mirrored clip for ' { sourceClip . name } ' used by ' { finalClipName } '" ); 434try { 435clipToUse = MirrorAnimationClipInMemory ( sourceClip ); 436if ( clipToUse == null ) { 437Debug . LogError ( $"Failed to generate in-memory mirrored clip for ' { sourceClip . name } '. Using source clip instead." ); 438clipToUse = sourceClip ; // Fallback to source 439} else { 440Debug . Log ( $"Successfully generated in-memory mirrored clip for ' { sourceClip . name } '" ); 441} 442} catch ( Exception e ) { 443Debug . LogError ( $"Error generating in-memory mirrored clip for ' { sourceClip . name } ': { e . Message } . Using source clip instead." ); 444clipToUse = sourceClip ; // Fallback to source on error 445} 446} else { 447// Not mirrored, use the loaded source clip directly 448clipToUse = sourceClip ; 449Debug . Log ( $"Using external clip ' { finalClipName } ' for toggle ' { originalToggleSpec . name } ' from path: { sourceClipPath } " ); 450} 451 452if ( clipToUse != null ) { 453// Important: Assign a unique name to the in-memory clip instance if it was mirrored, 454// otherwise Unity might get confused if multiple states reference the same in-memory clip object. 455// We use the finalClipName which should be unique within the context of this generator run. 456clipToUse . name = finalClipName ; 457animationClips [ finalClipName ] = clipToUse ; 458} 459 460} else { 461// Generate internal clip using the potentially modified GeneratedAnimationClipConfig 462AnimationClip internalClip = CreateAnimationClipFromConfig ( finalAnimConfig ); // Pass the final config 463// Ensure the internal clip also has a unique name matching its key 464internalClip . name = finalClipName ; 465animationClips [ finalClipName ] = internalClip ; 466Debug . Log ( $"Generated internal clip ' { finalClipName } ' for toggle ' { originalToggleSpec . name } '" ); 467} 468} 469Debug . Log ( "--- Finished Preparing Final Animation Clips ---" ); 470 471// Create actual assets. 472GenerateVRChatAssets ( config . toggles , vrcParams , vrcMenu ); 473AnimatorController controller = GenerateAnimatorController ( genAnimatorConfig ); // Pass the final config 474 475Debug . Log ( "=== Animator Generation Process Complete ===" ); 476return controller ; 477} 478 479private static void CreateAnimationClips ( GeneratedAnimationsConfig animationsConfig ) { 480foreach ( var clipConfig in animationsConfig . animations ) { 481AnimationClip newClip = new AnimationClip (); 482newClip . name = clipConfig . name ; 483 484// Apply mesh toggles 485foreach ( var meshToggle in clipConfig . meshToggles ) { 486AnimationCurve curve = new AnimationCurve ( new Keyframe ( 0 , meshToggle . value )); 487EditorCurveBinding binding = new EditorCurveBinding (); 488binding . path = meshToggle . path ; 489binding . type = typeof ( GameObject ); 490binding . propertyName = "m_IsActive" ; 491AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 492} 493 494// Apply blend shapes 495foreach ( var blendShape in clipConfig . blendShapes ) { 496AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , blendShape . value ); 497EditorCurveBinding binding = new EditorCurveBinding (); 498binding . path = blendShape . path ; 499binding . type = typeof ( SkinnedMeshRenderer ); 500binding . propertyName = "blendShape." + blendShape . blendShape ; 501AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 502} 503 504// Apply shader toggles 505foreach ( var shaderToggle in clipConfig . shaderToggles ) { 506AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , shaderToggle . value ); 507EditorCurveBinding binding = new EditorCurveBinding (); 508binding . path = shaderToggle . path ; 509 510// Use the specified renderer type 511if ( shaderToggle . rendererType == "MeshRenderer" ) { 512binding . type = typeof ( MeshRenderer ); 513} else { 514binding . type = typeof ( SkinnedMeshRenderer ); // Default or when explicitly specified 515} 516 517binding . propertyName = $"material. { shaderToggle . materialProperty } " ; 518AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 519} 520 521// Apply parent constraint weights 522foreach ( var parentConstraint in clipConfig . parentConstraintWeights ) { 523AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , parentConstraint . value ); 524EditorCurveBinding binding = new EditorCurveBinding (); 525binding . path = parentConstraint . path ; 526binding . type = typeof ( UnityEngine . Animations . ParentConstraint ); 527binding . propertyName = "m_Weight" ; 528AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 529} 530 531// Store in memory 532animationClips [ clipConfig . name ] = newClip ; 533Debug . Log ( "Created animation clip " + clipConfig . name ); 534} 535} 536 537private static AnimatorController GenerateAnimatorController ( GeneratedAnimatorConfig animatorConfig ) { 538AnimatorController controller = new AnimatorController (); 539 540// Add weight parameter used to ensure that the blendtrees always 541// run. All layers use this. Documented on vrc.school: 542// http://vrc.school/docs/Other/DBT-Combining#ed504c95853f4924adeffb6b125234ad 543List < AnimatorControllerParameter > parameters_list = new List < AnimatorControllerParameter > (); 544var yots_weight = new AnimatorControllerParameter (); 545yots_weight . name = "YOTS_Weight" ; 546yots_weight . type = AnimatorControllerParameterType . Float ; 547yots_weight . defaultFloat = 1.0f ; 548parameters_list . Add ( yots_weight ); 549// Add all other parameters 550foreach ( var param in animatorConfig . parameters ) { 551var p = new AnimatorControllerParameter (); 552p . name = param . name ; 553// Note: Parameter type is always Float, even for toggles, because blend trees use floats. 554// The VRCExpressionParameters handle the Bool/Float distinction for the menu. 555p . type = AnimatorControllerParameterType . Float ; 556p . defaultFloat = param . defaultValue ; 557parameters_list . Add ( p ); 558} 559controller . parameters = parameters_list . ToArray (); 560 561// Add base layer. This is structured as a wide direct blendtree 562// (DBT) comprised of blendtrees animating pairs of On/Off 563// animations. 564var baseLayerConfig = animatorConfig . layers [ 0 ]; 565var baseStateMachine = new AnimatorStateMachine (); 566baseStateMachine . name = "YOTS_BaseLayer_SM" ; 567 568var rootBlendTree = new BlendTree (); 569rootBlendTree . name = "YOTS_BaseLayer_RootBlendTree" ; 570rootBlendTree . blendType = BlendTreeType . Direct ; 571 572// Group animations by their base name (without _On/_Off suffix) to pair them 573var animationPairs = new Dictionary < string , List < AnimatorDirectBlendTreeEntry >> (); 574foreach ( var entry in baseLayerConfig . directBlendTree . entries ) { 575string baseName = entry . name ; 576if ( baseName . EndsWith ( "_On" )) 577baseName = baseName . Substring ( 0 , baseName . Length - "_On" . Length ); 578else if ( baseName . EndsWith ( "_Off" )) 579baseName = baseName . Substring ( 0 , baseName . Length - "_Off" . Length ); 580 581if ( ! animationPairs . ContainsKey ( baseName )) 582animationPairs [ baseName ] = new List < AnimatorDirectBlendTreeEntry > (); 583animationPairs [ baseName ]. Add ( entry ); 584} 585 586// Create a blend tree for each animation pair 587foreach ( var pair in animationPairs ) { 588var animations = pair . Value ; 589if ( animations . Count == 0 ) continue ; 590 591// Get thresholds from the first animation (they should all be the same for this pair) 592var firstAnim = animations [ 0 ]; 593var param = firstAnim . parameter ; 594float offThreshold = firstAnim . offThreshold ; 595float onThreshold = firstAnim . onThreshold ; 596 597// Create a blendtree controlled by this toggle's parameter. 598var paramBlendTree = new BlendTree (); 599paramBlendTree . name = $"YOTS_BlendTree_ { pair . Key } " ; 600paramBlendTree . blendType = BlendTreeType . Simple1D ; 601paramBlendTree . blendParameter = param ; 602 603// Handle inverted thresholds (e.g., offThreshold=0.5, onThreshold=0.0) 604float minThreshold = Mathf . Min ( offThreshold , onThreshold ); 605float maxThreshold = Mathf . Max ( offThreshold , onThreshold ); 606paramBlendTree . minThreshold = minThreshold ; 607paramBlendTree . maxThreshold = maxThreshold ; 608paramBlendTree . useAutomaticThresholds = false ; 609 610var children = new List < ChildMotion > (); 611 612// Build list of animations with their thresholds 613var animsWithThresholds = new List < ( AnimatorDirectBlendTreeEntry entry , float threshold ) > (); 614foreach ( var animation in animations ) { 615float threshold = animation . name . EndsWith ( "_On" ) ? onThreshold : offThreshold ; 616animsWithThresholds . Add (( animation , threshold )); 617} 618 619// Sort by threshold value (ascending) to ensure Unity interpolates correctly 620foreach ( var ( animation , threshold ) in animsWithThresholds . OrderBy ( a=> a . threshold )) { 621Debug . Log ( $"Adding child motion for: { animation . name } at threshold { threshold } " ); 622if ( ! animationClips . TryGetValue ( animation . name , out AnimationClip clip )) { 623throw new InvalidOperationException ( $"Animation clip not found in memory: { animation . name } " ); 624} 625 626children . Add ( new ChildMotion { 627motion = clip , 628timeScale = 1f , 629threshold = threshold 630}); 631} 632paramBlendTree . children = children . ToArray (); 633 634// Add that blendtree to the parent direct blendtree (DBT) 635// controlled by YOTS_Weight. That YOTS_Weight parameter is 636// always set to 1, so the child blendtree always runs. 637rootBlendTree . children = rootBlendTree . children . Append ( 638new ChildMotion { 639motion = paramBlendTree , 640timeScale = 1f , 641directBlendParameter = "YOTS_Weight" 642}). ToArray (); 643} 644 645var baseState = baseStateMachine . AddState ( "YOTS_BaseLayer_State" ); 646baseState . motion = rootBlendTree ; 647baseState . writeDefaultValues = true ; 648baseStateMachine . defaultState = baseState ; 649 650controller . AddLayer ( new AnimatorControllerLayer { 651name = "YOTS_BaseLayer" , 652defaultWeight = 1.0f , 653stateMachine = baseStateMachine 654}); 655 656// Add override layers. These are DBTs of On animations (no Off 657// animations). 658for ( int i = 1 ; i < animatorConfig . layers . Count ; i ++ ) { 659var layerConfig = animatorConfig . layers [ i ]; 660string layerName = $"YOTS_OverrideLayer {( i - 1 ). ToString ( "00" )} " ; 661 662var stateMachine = new AnimatorStateMachine (); 663stateMachine . name = layerName + "_SM" ; 664 665var blendTree = new BlendTree (); 666blendTree . name = layerName + "_BlendTree" ; 667blendTree . blendType = BlendTreeType . Direct ; 668 669foreach ( var entry in layerConfig . directBlendTree . entries ) { 670if ( ! animationClips . TryGetValue ( entry . name , out AnimationClip clip )) { 671throw new InvalidOperationException ( $"Animation clip not found in memory: { entry . name } " ); 672} 673 674blendTree . children = blendTree . children . Append ( new ChildMotion { 675motion = clip , 676timeScale = 1f , 677directBlendParameter = entry . parameter 678}). ToArray (); 679} 680 681var state = stateMachine . AddState ( layerName + "_State" ); 682state . motion = blendTree ; 683state . writeDefaultValues = true ; 684stateMachine . defaultState = state ; 685 686controller . AddLayer ( new AnimatorControllerLayer { 687name = layerName , 688defaultWeight = 1.0f , 689stateMachine = stateMachine 690}); 691 692Debug . Log ( $"Added override layer: { layerName } " ); 693} 694 695return controller ; 696} 697 698private static Dictionary < string , int > TopologicalSortToggles ( List < ToggleSpec > toggleSpecs ) { 699// Group toggles by parameter name to handle shared parameters 700var togglesByParam = toggleSpecs 701. GroupBy ( t=> t . GetParameterName ()) 702. ToDictionary ( g=> g . Key , g=> g . ToList ()); 703 704// Get mapping from toggle parameter name to children 705Dictionary < string , HashSet < string >> graph = new Dictionary < string , HashSet < string >> (); 706Dictionary < string , HashSet < string >> dependencyNames = new Dictionary < string , HashSet < string >> (); 707 708foreach ( var paramGroup in togglesByParam ) { 709string paramName = paramGroup . Key ; 710if ( ! graph . ContainsKey ( paramName )) 711graph [ paramName ] = new HashSet < string > (); 712if ( ! dependencyNames . ContainsKey ( paramName )) 713dependencyNames [ paramName ] = new HashSet < string > (); 714 715// Collect all dependencies from all toggles that share this parameter 716foreach ( var toggle in paramGroup . Value ) { 717foreach ( var dep in toggle . dependencies ) { 718// Find the toggle with this dependency name 719var depToggle = toggleSpecs . FirstOrDefault ( t=> t . name == dep ); 720if ( depToggle == null ) { 721throw new ArgumentException ( $"Toggle ' { toggle . name } ' has dependency ' { dep } ' that doesn't exist" ); 722} 723string depParamName = depToggle . GetParameterName (); 724if ( ! graph . ContainsKey ( depParamName )) 725graph [ depParamName ] = new HashSet < string > (); 726graph [ depParamName ]. Add ( paramName ); 727dependencyNames [ paramName ]. Add ( dep ); 728} 729} 730} 731 732Dictionary < string , int > inDegree = new Dictionary < string , int > (); 733foreach ( var paramGroup in togglesByParam ) { 734string paramName = paramGroup . Key ; 735inDegree [ paramName ] = dependencyNames [ paramName ]. Count ; 736} 737 738Dictionary < string , int > depths = new Dictionary < string , int > (); 739Queue < string > queue = new Queue < string > (); 740 741// Identify start nodes 742foreach ( var pair in inDegree ) { 743if ( pair . Value == 0 ) { 744queue . Enqueue ( pair . Key ); 745depths [ pair . Key ] = 0 ; 746} 747} 748 749int processedNodes = 0 ; 750while ( queue . Count > 0 ) { 751// Pop start nodes one by one. 752string current = queue . Dequeue (); 753processedNodes ++ ; 754int currentDepth = depths [ current ]; 755// Enqueue children and set their depth to cur depth + 1. 756foreach ( var child in graph [ current ]) { 757inDegree [ child ] -- ; 758if ( inDegree [ child ] == 0 ) { 759queue . Enqueue ( child ); 760depths [ child ] = currentDepth + 1 ; 761} 762} 763} 764 765// Check if all unique parameter names were processed 766if ( processedNodes != togglesByParam . Count ) { 767var unprocessedParams = togglesByParam . Keys 768. Where ( p=> ! depths . ContainsKey ( p )) 769. ToList (); 770 771// Collect all toggle names that are part of the cycle 772var cycleNodes = new List < string > (); 773foreach ( var param in unprocessedParams ) { 774cycleNodes . AddRange ( togglesByParam [ param ]. Select ( t=> t . name )); 775} 776 777// Provide detailed error message 778if ( cycleNodes . Count == 0 ) { 779// This should never happen. 780throw new ArgumentException ( $"Dependency cycle detected but couldn't identify specific nodes. Unprocessed parameters: { string . Join ( ", " , unprocessedParams )} " ); 781} else { 782throw new ArgumentException ( $"Dependency cycle detected in toggle specifications. Nodes involved: { string . Join ( ", " , cycleNodes )} " ); 783} 784} 785 786return depths ; 787} 788 789private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig ( List < ToggleSpec > toggleSpecs ) { 790GeneratedAnimatorConfig genAnimatorConfig = new GeneratedAnimatorConfig (); 791// Sort toggles into layers 792Dictionary < string , int > depths = TopologicalSortToggles ( toggleSpecs ); 793var togglesByDepth = toggleSpecs 794. GroupBy ( t=> depths [ t . GetParameterName ()]) 795. OrderBy ( g=> g . Key ) 796. ToList (); 797// Add layers 798for ( int i = 0 ; i < togglesByDepth . Count ; i ++ ) { 799var depthGroup = togglesByDepth [ i ]; 800AnimatorLayer layer = new AnimatorLayer (); 801layer . name = i == 0 ? "YOTS_BaseLayer" : $"YOTS_OverrideLayer {( i - 1 ). ToString ( "00" )} " ; 802foreach ( var toggle in depthGroup ) { 803string paramName = toggle . GetParameterName (); 804if ( ! genAnimatorConfig . parameters . Any ( p=> p . name == paramName )) 805// Populate thresholds when adding parameter settings 806genAnimatorConfig . parameters . Add ( new AnimatorParameterSetting { 807name = paramName , 808defaultValue = toggle . defaultValue , 809offThreshold = toggle . offThreshold , 810onThreshold = toggle . onThreshold 811}); 812 813// Use a unique name for animations when toggles share parameters 814string animName = paramName ; 815if ( toggleSpecs . Count ( t=> t . GetParameterName () == paramName ) > 1 ) { 816// Make animation names unique by including toggle name 817animName = paramName + "_" + toggle . name ; 818} 819 820layer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 821name = animName + "_On" , 822parameter = paramName , 823offThreshold = toggle . offThreshold , 824onThreshold = toggle . onThreshold 825}); 826 827layer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 828name = animName + "_Off" , 829parameter = paramName , 830offThreshold = toggle . offThreshold , 831onThreshold = toggle . onThreshold 832}); 833} 834genAnimatorConfig . layers . Add ( layer ); 835} 836// Add animations 837GeneratedAnimationsConfig animConfig = GenerateAnimationConfig ( toggleSpecs ); 838genAnimatorConfig . animations = animConfig . animations ; 839return genAnimatorConfig ; 840} 841 842private static GeneratedAnimationsConfig GenerateAnimationConfig ( List < ToggleSpec > toggleSpecs ) { 843// This function is now only used to populate the initial GeneratedAnimatorConfig. 844// The actual clip creation or loading happens later in GenerateAnimator. 845GeneratedAnimationsConfig genAnimConfig = new GeneratedAnimationsConfig (); 846foreach ( var toggle in toggleSpecs ) { 847string paramName = toggle . GetParameterName (); 848 849// Use a unique name for animations when toggles share parameters 850string animName = paramName ; 851if ( toggleSpecs . Count ( t=> t . GetParameterName () == paramName ) > 1 ) { 852// Make animation names unique by including toggle name 853animName = paramName + "_" + toggle . name ; 854} 855 856// We still create dummy entries here so ApplyIndependentFixToAnimatorConfig etc. have something to work with. 857// The *content* of these might not be used if external clips are provided. 858var ( onConfig , offConfig ) = GenerateSingleToggleAnimationConfigs ( toggle , animName ); 859genAnimConfig . animations . Add ( onConfig ); 860genAnimConfig . animations . Add ( offConfig ); 861} 862return genAnimConfig ; 863} 864 865private static ( GeneratedAnimationClipConfig onConfig , GeneratedAnimationClipConfig offConfig ) 866GenerateSingleToggleAnimationConfigs ( ToggleSpec toggle , string animName ) { 867string paramName = toggle . GetParameterName (); 868 869GeneratedAnimationClipConfig onAnim = new GeneratedAnimationClipConfig (); 870onAnim . name = animName + "_On" ; 871if ( toggle . meshToggles != null ) { 872foreach ( var mesh in toggle . meshToggles ) { 873onAnim . meshToggles . Add ( new GeneratedMeshToggle { path = mesh , value = 1.0f }); 874} 875} 876if ( toggle . inverseMeshToggles != null ) { 877foreach ( var mesh in toggle . inverseMeshToggles ) { 878onAnim . meshToggles . Add ( new GeneratedMeshToggle { path = mesh , value = 0.0f }); 879} 880} 881if ( toggle . blendShapes != null ) { 882foreach ( var bs in toggle . blendShapes ) { 883// Validate that either path or paths is specified 884if ( string . IsNullOrEmpty ( bs . path ) && ( bs . paths == null || bs . paths . Count == 0 )) { 885throw new ArgumentException ( $"Blend shape in ' { toggle . name } ' must specify either 'path' or 'paths'" ); 886} 887// Validate that either blendShape or blendShapes is specified 888if ( string . IsNullOrEmpty ( bs . blendShape ) && ( bs . blendShapes == null || bs . blendShapes . Count == 0 )) { 889throw new ArgumentException ( $"Blend shape in ' { toggle . name } ' must specify either 'blendShape' or 'blendShapes'" ); 890} 891 892// Use toggle's onValue if blendshape is using the default (100.0f) 893float effectiveOnValue = ( bs . onValue == 100.0f ) ? toggle . onValue : bs . onValue ; 894 895// Collect all blendshape names 896List < string > allBlendShapes = new List < string > (); 897if ( ! string . IsNullOrEmpty ( bs . blendShape )) { 898allBlendShapes . Add ( bs . blendShape ); 899} 900if ( bs . blendShapes != null ) { 901allBlendShapes . AddRange ( bs . blendShapes ); 902} 903 904foreach ( var blendShapeName in allBlendShapes ) { 905// Handle single path 906if ( ! string . IsNullOrEmpty ( bs . path )) { 907onAnim . blendShapes . Add ( new GeneratedBlendShape { 908path = bs . path , 909blendShape = blendShapeName , 910value = effectiveOnValue 911}); 912} 913 914// Handle multiple paths 915if ( bs . paths != null ) { 916foreach ( var path in bs . paths ) { 917onAnim . blendShapes . Add ( new GeneratedBlendShape { 918path = path , 919blendShape = blendShapeName , 920value = effectiveOnValue 921}); 922} 923} 924} 925} 926} 927if ( toggle . shaderToggles != null ) { 928foreach ( var st in toggle . shaderToggles ) { 929if ( string . IsNullOrEmpty ( st . path ) && ( st . paths == null || st . paths . Count == 0 )) { 930throw new ArgumentException ( $"Shader toggle in ' { toggle . name } ' must specify either 'path' or 'paths'" ); 931} 932if ( ! string . IsNullOrEmpty ( st . path )) { 933onAnim . shaderToggles . Add ( new GeneratedShaderToggle { 934path = st . path , materialProperty = st . materialProperty , value = st . onValue , rendererType = st . rendererType 935}); 936} 937if ( st . paths != null ) { 938foreach ( var path in st . paths ) { 939onAnim . shaderToggles . Add ( new GeneratedShaderToggle { 940path = path , materialProperty = st . materialProperty , value = st . onValue , rendererType = st . rendererType 941}); 942} 943} 944} 945} 946if ( toggle . parentConstraintWeights != null ) { 947foreach ( var pc in toggle . parentConstraintWeights ) { 948onAnim . parentConstraintWeights . Add ( new GeneratedParentConstraint { path = pc . path , value = pc . onValue }); 949} 950} 951 952GeneratedAnimationClipConfig offAnim = new GeneratedAnimationClipConfig (); 953offAnim . name = animName + "_Off" ; 954if ( toggle . meshToggles != null ) { 955foreach ( var mesh in toggle . meshToggles ) { 956offAnim . meshToggles . Add ( new GeneratedMeshToggle { path = mesh , value = 0.0f }); 957} 958} 959if ( toggle . inverseMeshToggles != null ) { 960foreach ( var mesh in toggle . inverseMeshToggles ) { 961offAnim . meshToggles . Add ( new GeneratedMeshToggle { path = mesh , value = 1.0f }); 962} 963} 964if ( toggle . blendShapes != null ) { 965foreach ( var bs in toggle . blendShapes ) { 966// Validate that either path or paths is specified 967if ( string . IsNullOrEmpty ( bs . path ) && ( bs . paths == null || bs . paths . Count == 0 )) { 968throw new ArgumentException ( $"Blend shape in ' { toggle . name } ' must specify either 'path' or 'paths'" ); 969} 970// Validate that either blendShape or blendShapes is specified 971if ( string . IsNullOrEmpty ( bs . blendShape ) && ( bs . blendShapes == null || bs . blendShapes . Count == 0 )) { 972throw new ArgumentException ( $"Blend shape in ' { toggle . name } ' must specify either 'blendShape' or 'blendShapes'" ); 973} 974 975// Use toggle's offValue if blendshape is using the default (0.0f) 976float effectiveOffValue = ( bs . offValue == 0.0f ) ? toggle . offValue : bs . offValue ; 977 978// Collect all blendshape names 979List < string > allBlendShapes = new List < string > (); 980if ( ! string . IsNullOrEmpty ( bs . blendShape )) { 981allBlendShapes . Add ( bs . blendShape ); 982} 983if ( bs . blendShapes != null ) { 984allBlendShapes . AddRange ( bs . blendShapes ); 985} 986 987foreach ( var blendShapeName in allBlendShapes ) { 988// Handle single path 989if ( ! string . IsNullOrEmpty ( bs . path )) { 990offAnim . blendShapes . Add ( new GeneratedBlendShape { 991path = bs . path , 992blendShape = blendShapeName , 993value = effectiveOffValue 994}); 995} 996 997// Handle multiple paths 998if ( bs . paths != null ) { 999foreach ( var path in bs . paths ) { 1000offAnim . blendShapes . Add ( new GeneratedBlendShape { 1001path = path , 1002blendShape = blendShapeName , 1003value = effectiveOffValue 1004}); 1005} 1006} 1007} 1008} 1009} 1010if ( toggle . shaderToggles != null ) { 1011foreach ( var st in toggle . shaderToggles ) { 1012if ( string . IsNullOrEmpty ( st . path ) && ( st . paths == null || st . paths . Count == 0 )) { 1013throw new ArgumentException ( $"Shader toggle in ' { toggle . name } ' must specify either 'path' or 'paths'" ); 1014} 1015if ( ! string . IsNullOrEmpty ( st . path )) { 1016offAnim . shaderToggles . Add ( new GeneratedShaderToggle { 1017path = st . path , materialProperty = st . materialProperty , value = st . offValue , rendererType = st . rendererType 1018}); 1019} 1020if ( st . paths != null ) { 1021foreach ( var path in st . paths ) { 1022offAnim . shaderToggles . Add ( new GeneratedShaderToggle { 1023path = path , materialProperty = st . materialProperty , value = st . offValue , rendererType = st . rendererType 1024}); 1025} 1026} 1027} 1028} 1029if ( toggle . parentConstraintWeights != null ) { 1030foreach ( var pc in toggle . parentConstraintWeights ) { 1031offAnim . parentConstraintWeights . Add ( new GeneratedParentConstraint { path = pc . path , value = pc . offValue }); 1032} 1033} 1034 1035return ( onAnim , offAnim ); 1036} 1037 1038private static AnimationClip CreateAnimationClipFromConfig ( GeneratedAnimationClipConfig clipConfig ) { 1039AnimationClip newClip = new AnimationClip (); 1040newClip . name = clipConfig . name ; 1041 1042// Apply mesh toggles 1043foreach ( var meshToggle in clipConfig . meshToggles ) { 1044AnimationCurve curve = new AnimationCurve ( new Keyframe ( 0 , meshToggle . value )); 1045EditorCurveBinding binding = EditorCurveBinding . FloatCurve ( meshToggle . path , typeof ( GameObject ), "m_IsActive" ); 1046AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 1047} 1048 1049// Apply blend shapes 1050foreach ( var blendShape in clipConfig . blendShapes ) { 1051AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , blendShape . value ); 1052EditorCurveBinding binding = EditorCurveBinding . FloatCurve ( blendShape . path , typeof ( SkinnedMeshRenderer ), "blendShape." + blendShape . blendShape ); 1053AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 1054} 1055 1056// Apply shader toggles 1057foreach ( var shaderToggle in clipConfig . shaderToggles ) { 1058AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , shaderToggle . value ); 1059Type rendererType = shaderToggle . rendererType == "MeshRenderer" ? typeof ( MeshRenderer ) : typeof ( SkinnedMeshRenderer ); 1060EditorCurveBinding binding = EditorCurveBinding . FloatCurve ( shaderToggle . path , rendererType , $"material. { shaderToggle . materialProperty } " ); 1061AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 1062} 1063 1064// Apply parent constraint weights 1065foreach ( var parentConstraint in clipConfig . parentConstraintWeights ) { 1066AnimationCurve curve = AnimationCurve . Constant ( 0 , 0 , parentConstraint . value ); 1067EditorCurveBinding binding = EditorCurveBinding . FloatCurve ( parentConstraint . path , typeof ( UnityEngine . Animations . ParentConstraint ), "m_Weight" ); 1068AnimationUtility . SetEditorCurve ( newClip , binding , curve ); 1069} 1070 1071return newClip ; 1072} 1073 1074private static GeneratedAnimatorConfig ApplyIndependentFixToAnimatorConfig ( GeneratedAnimatorConfig genAnimatorConfig ) { 1075// TODO meshToggles do not implement offValue/onValue at the JSON level, 1076// so this is redundant. 1077float GetOffValueForMesh ( string path , List < GeneratedMeshToggle > offList ) { 1078var offToggle = offList ? . FirstOrDefault ( mt=> mt . path == path ); 1079return offToggle != null ? offToggle . value : 0.0f ; 1080} 1081 1082float GetOffValueForBlend ( string path , string blendShapeName , List < GeneratedBlendShape > offList ) { 1083var offBlend = offList ? . FirstOrDefault ( bs=> bs . path == path && bs . blendShape == blendShapeName ); 1084return offBlend != null ? offBlend . value : 0.0f ; 1085} 1086 1087float GetOffValueForShader ( string path , string materialProperty , List < GeneratedShaderToggle > offList ) { 1088var offShader = offList ? . FirstOrDefault ( st=> st . path == path && st . materialProperty == materialProperty ); 1089return offShader != null ? offShader . value : 0.0f ; 1090} 1091 1092float GetOffValueForParentConstraint ( string path , List < GeneratedParentConstraint > offList ) { 1093var offPC = offList ? . FirstOrDefault ( pc=> pc . path == path ); 1094return offPC != null ? offPC . value : 0.0f ; 1095} 1096 1097// Create mapping from toggle name -> (on animation, off animation) 1098Dictionary < string , ( GeneratedAnimationClipConfig on , GeneratedAnimationClipConfig off ) > toggleAnimations = 1099new Dictionary < string , ( GeneratedAnimationClipConfig , GeneratedAnimationClipConfig ) > (); 1100foreach ( var anim in genAnimatorConfig . animations ) { 1101if ( anim . name . EndsWith ( "_On" )) { 1102string toggleName = anim . name . Substring ( 0 , anim . name . LastIndexOf ( "_On" )); 1103if ( ! toggleAnimations . ContainsKey ( toggleName )) 1104toggleAnimations [ toggleName ] = ( null , null ); 1105var pair = toggleAnimations [ toggleName ]; 1106pair . on = anim ; 1107toggleAnimations [ toggleName ] = pair ; 1108} 1109else if ( anim . name . EndsWith ( "_Off" )) { 1110string toggleName = anim . name . Substring ( 0 , anim . name . LastIndexOf ( "_Off" )); 1111if ( ! toggleAnimations . ContainsKey ( toggleName )) 1112toggleAnimations [ toggleName ] = ( null , null ); 1113var pair = toggleAnimations [ toggleName ]; 1114pair . off = anim ; 1115toggleAnimations [ toggleName ] = pair ; 1116} 1117} 1118 1119Dictionary < string , int > toggleToLayerIndex = new Dictionary < string , int > (); 1120for ( int i = 0 ; i < genAnimatorConfig . layers . Count ; i ++ ) { 1121var layer = genAnimatorConfig . layers [ i ]; 1122foreach ( var entry in layer . directBlendTree . entries ) { 1123string entryName = entry . name ; 1124string toggleName = entryName ; 1125if ( toggleName . EndsWith ( "_On" )) 1126toggleName = toggleName . Substring ( 0 , toggleName . Length - "_On" . Length ); 1127else if ( toggleName . EndsWith ( "_Off" )) 1128toggleName = toggleName . Substring ( 0 , toggleName . Length - "_Off" . Length ); 1129if ( ! toggleToLayerIndex . ContainsKey ( toggleName )) 1130toggleToLayerIndex [ toggleName ] = i ; 1131} 1132} 1133 1134// Mapping from attribute touched by animation to the set of toggles 1135// which affect it. 1136Dictionary < string , HashSet < string >> attributeToToggles = new Dictionary < string , HashSet < string >> (); 1137foreach ( var kvp in toggleAnimations ) { 1138string toggleName = kvp . Key ; 1139var pair = kvp . Value ; 1140if ( pair . on == null ) continue ; 1141 1142HashSet < string > attributes = new HashSet < string > (); 1143if ( pair . on . meshToggles != null ) { 1144foreach ( var mt in pair . on . meshToggles ) { 1145string attr = GetMeshToggleAttributeId ( mt . path ); 1146attributes . Add ( attr ); 1147} 1148} 1149if ( pair . on . blendShapes != null ) { 1150foreach ( var bs in pair . on . blendShapes ) { 1151string attr = GetBlendShapeAttributeId ( bs . path , bs . blendShape ); 1152attributes . Add ( attr ); 1153} 1154} 1155if ( pair . on . shaderToggles != null ) { 1156foreach ( var st in pair . on . shaderToggles ) { 1157string attr = GetShaderToggleAttributeId ( st . path , st . materialProperty ); 1158attributes . Add ( attr ); 1159} 1160} 1161// Add parent constraint attributes 1162if ( pair . on . parentConstraintWeights != null ) { 1163foreach ( var pc in pair . on . parentConstraintWeights ) { 1164string attr = GetParentConstraintAttributeId ( pc . path ); 1165attributes . Add ( attr ); 1166} 1167} 1168foreach ( var attr in attributes ) { 1169if ( ! attributeToToggles . TryGetValue ( attr , out var set )) { 1170set = new HashSet < string > (); 1171attributeToToggles [ attr ] = set ; 1172} 1173set . Add ( toggleName ); 1174} 1175} 1176 1177// TODO assert that all toggles affecting the same attribute are on 1178// different layers. 1179 1180List < GeneratedAnimationClipConfig > newAnimations = new List < GeneratedAnimationClipConfig > (); 1181 1182AnimatorLayer baseLayer = genAnimatorConfig . layers . FirstOrDefault ( l=> l . name == "BaseLayer" ); 1183if ( baseLayer == null && genAnimatorConfig . layers . Count > 0 ) 1184baseLayer = genAnimatorConfig . layers [ 0 ]; 1185 1186foreach ( var kvp in toggleAnimations ) { 1187string toggleName = kvp . Key ; 1188var pair = kvp . Value ; 1189int layerIndex = toggleToLayerIndex [ toggleName ]; 1190 1191if ( layerIndex == 0 ) { 1192newAnimations . Add ( pair . on ); 1193newAnimations . Add ( pair . off ); 1194continue ; 1195} 1196 1197// Work out which of the animation's mesh toggles are overrides and 1198// which are independent. 1199List < GeneratedMeshToggle > independentMesh = new List < GeneratedMeshToggle > (); 1200List < GeneratedMeshToggle > dependentMesh = new List < GeneratedMeshToggle > (); 1201if ( pair . on . meshToggles != null ) { 1202foreach ( var mt in pair . on . meshToggles ) { 1203string attr = GetMeshToggleAttributeId ( mt . path ); 1204if ( attributeToToggles [ attr ]. Count == 1 ) 1205independentMesh . Add ( mt ); 1206else 1207dependentMesh . Add ( mt ); 1208} 1209} 1210 1211// Work out which of the animation's blendshapes are overrides and 1212// which are independent. 1213List < GeneratedBlendShape > independentBlend = new List < GeneratedBlendShape > (); 1214List < GeneratedBlendShape > dependentBlend = new List < GeneratedBlendShape > (); 1215if ( pair . on . blendShapes != null ) { 1216foreach ( var bs in pair . on . blendShapes ) { 1217string attr = GetBlendShapeAttributeId ( bs . path , bs . blendShape ); 1218if ( attributeToToggles [ attr ]. Count == 1 ) 1219independentBlend . Add ( bs ); 1220else 1221dependentBlend . Add ( bs ); 1222} 1223} 1224 1225// Work out which of the animation's shader toggles are overrides and which are independent 1226List < GeneratedShaderToggle > independentShader = new List < GeneratedShaderToggle > (); 1227List < GeneratedShaderToggle > dependentShader = new List < GeneratedShaderToggle > (); 1228if ( pair . on . shaderToggles != null ) { 1229foreach ( var st in pair . on . shaderToggles ) { 1230string attr = GetShaderToggleAttributeId ( st . path , st . materialProperty ); 1231if ( attributeToToggles [ attr ]. Count == 1 ) 1232independentShader . Add ( st ); 1233else 1234dependentShader . Add ( st ); 1235} 1236} 1237 1238// Handle parent constraints the same way as other animated properties 1239List < GeneratedParentConstraint > independentParentConstraint = new List < GeneratedParentConstraint > (); 1240List < GeneratedParentConstraint > dependentParentConstraint = new List < GeneratedParentConstraint > (); 1241if ( pair . on . parentConstraintWeights != null ) { 1242foreach ( var pc in pair . on . parentConstraintWeights ) { 1243string attr = GetParentConstraintAttributeId ( pc . path ); 1244if ( attributeToToggles [ attr ]. Count == 1 ) 1245independentParentConstraint . Add ( pc ); 1246else 1247dependentParentConstraint . Add ( pc ); 1248} 1249} 1250 1251bool hasIndependent = ( independentMesh . Count > 0 || independentBlend . Count > 0 || 1252independentShader . Count > 0 || independentParentConstraint . Count > 0 ); 1253bool hasDependent = ( dependentMesh . Count > 0 || dependentBlend . Count > 0 || 1254dependentShader . Count > 0 || dependentParentConstraint . Count > 0 ); 1255 1256if ( hasIndependent && hasDependent ) { 1257GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig (); 1258dependentOn . name = toggleName + "_Dependent_On" ; 1259dependentOn . meshToggles = dependentMesh ; 1260dependentOn . blendShapes = dependentBlend ; 1261dependentOn . shaderToggles = dependentShader ; 1262dependentOn . parentConstraintWeights = dependentParentConstraint ; 1263 1264GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig (); 1265dependentOff . name = toggleName + "_Dependent_Off" ; 1266dependentOff . meshToggles = dependentMesh 1267. Select ( mt=> new GeneratedMeshToggle { 1268path = mt . path , 1269value = GetOffValueForMesh ( mt . path , pair . off . meshToggles ) 1270}) 1271. ToList (); 1272dependentOff . blendShapes = dependentBlend 1273. Select ( bs=> new GeneratedBlendShape { 1274path = bs . path , 1275blendShape = bs . blendShape , 1276value = GetOffValueForBlend ( bs . path , bs . blendShape , pair . off . blendShapes ) 1277}) 1278. ToList (); 1279dependentOff . shaderToggles = dependentShader 1280. Select ( st=> new GeneratedShaderToggle { 1281path = st . path , 1282materialProperty = st . materialProperty , 1283value = GetOffValueForShader ( st . path , st . materialProperty , pair . off . shaderToggles ), 1284rendererType = st . rendererType 1285}) 1286. ToList (); 1287dependentOff . parentConstraintWeights = dependentParentConstraint 1288. Select ( pc=> new GeneratedParentConstraint { 1289path = pc . path , 1290value = GetOffValueForParentConstraint ( pc . path , pair . off . parentConstraintWeights ) 1291}) 1292. ToList (); 1293 1294GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig (); 1295independentOn . name = toggleName + "_Independent_On" ; 1296independentOn . meshToggles = independentMesh ; 1297independentOn . blendShapes = independentBlend ; 1298independentOn . shaderToggles = independentShader ; 1299independentOn . parentConstraintWeights = independentParentConstraint ; 1300 1301GeneratedAnimationClipConfig independentOff = new GeneratedAnimationClipConfig (); 1302independentOff . name = toggleName + "_Independent_Off" ; 1303independentOff . meshToggles = independentMesh 1304. Select ( mt=> new GeneratedMeshToggle { 1305path = mt . path , 1306value = GetOffValueForMesh ( mt . path , pair . off . meshToggles ) 1307}) 1308. ToList (); 1309independentOff . blendShapes = independentBlend 1310. Select ( bs=> new GeneratedBlendShape { 1311path = bs . path , 1312blendShape = bs . blendShape , 1313value = GetOffValueForBlend ( bs . path , bs . blendShape , pair . off . blendShapes ) 1314}) 1315. ToList (); 1316independentOff . shaderToggles = independentShader 1317. Select ( st=> new GeneratedShaderToggle { 1318path = st . path , 1319materialProperty = st . materialProperty , 1320value = GetOffValueForShader ( st . path , st . materialProperty , pair . off . shaderToggles ), 1321rendererType = st . rendererType 1322}) 1323. ToList (); 1324independentOff . parentConstraintWeights = independentParentConstraint 1325. Select ( pc=> new GeneratedParentConstraint { 1326path = pc . path , 1327value = GetOffValueForParentConstraint ( pc . path , pair . off . parentConstraintWeights ) 1328}) 1329. ToList (); 1330 1331newAnimations . Add ( dependentOn ); 1332newAnimations . Add ( dependentOff ); 1333newAnimations . Add ( independentOn ); 1334newAnimations . Add ( independentOff ); 1335 1336AnimatorLayer overrideLayer = genAnimatorConfig . layers [ layerIndex ]; 1337foreach ( var entry in overrideLayer . directBlendTree . entries ) { 1338if ( entry . name . StartsWith ( toggleName ) && 1339( entry . name . EndsWith ( "_On" ) || entry . name . EndsWith ( "_Off" ))) { 1340entry . name = entry . name . EndsWith ( "_On" ) ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off" ; 1341} 1342} 1343 1344if ( baseLayer != null ) { 1345baseLayer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 1346name = toggleName + "_Independent_On" , 1347parameter = toggleName 1348}); 1349baseLayer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 1350name = toggleName + "_Independent_Off" , 1351parameter = toggleName 1352}); 1353} 1354} else if ( hasIndependent ) { 1355GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig (); 1356independentOn . name = toggleName + "_Independent_On" ; 1357independentOn . meshToggles = pair . on . meshToggles ; 1358independentOn . blendShapes = pair . on . blendShapes ; 1359independentOn . shaderToggles = pair . on . shaderToggles ; 1360independentOn . parentConstraintWeights = pair . on . parentConstraintWeights ; 1361GeneratedAnimationClipConfig independentOff = new GeneratedAnimationClipConfig (); 1362independentOff . name = toggleName + "_Independent_Off" ; 1363independentOff . meshToggles = pair . off . meshToggles ; 1364independentOff . blendShapes = pair . off . blendShapes ; 1365independentOff . shaderToggles = pair . off . shaderToggles ; 1366independentOff . parentConstraintWeights = pair . off . parentConstraintWeights ; 1367 1368newAnimations . Add ( independentOn ); 1369newAnimations . Add ( independentOff ); 1370 1371AnimatorLayer overrideLayer = genAnimatorConfig . layers [ layerIndex ]; 1372overrideLayer . directBlendTree . entries . RemoveAll ( e=> e . name . StartsWith ( toggleName )); 1373if ( baseLayer != null ) { 1374baseLayer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 1375name = toggleName + "_Independent_On" , 1376parameter = toggleName 1377}); 1378baseLayer . directBlendTree . entries . Add ( new AnimatorDirectBlendTreeEntry { 1379name = toggleName + "_Independent_Off" , 1380parameter = toggleName 1381}); 1382} 1383} else if ( hasDependent ) { 1384GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig (); 1385dependentOn . name = toggleName + "_Dependent_On" ; 1386dependentOn . meshToggles = pair . on . meshToggles ; 1387dependentOn . blendShapes = pair . on . blendShapes ; 1388dependentOn . shaderToggles = pair . on . shaderToggles ; 1389dependentOn . parentConstraintWeights = pair . on . parentConstraintWeights ; 1390GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig (); 1391dependentOff . name = toggleName + "_Dependent_Off" ; 1392dependentOff . meshToggles = pair . off . meshToggles ; 1393dependentOff . blendShapes = pair . off . blendShapes ; 1394dependentOff . shaderToggles = pair . off . shaderToggles ; 1395dependentOff . parentConstraintWeights = pair . off . parentConstraintWeights ; 1396 1397newAnimations . Add ( dependentOn ); 1398newAnimations . Add ( dependentOff ); 1399 1400AnimatorLayer overrideLayer = genAnimatorConfig . layers [ layerIndex ]; 1401foreach ( var entry in overrideLayer . directBlendTree . entries ) { 1402if ( entry . name . StartsWith ( toggleName ) && 1403( entry . name . EndsWith ( "_On" ) || entry . name . EndsWith ( "_Off" ))) { 1404entry . name = entry . name . EndsWith ( "_On" ) ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off" ; 1405} 1406} 1407} else { 1408throw new ArgumentException ( $"Toggle { toggleName } seems to have no animations." ); 1409} 1410} 1411 1412genAnimatorConfig . animations = newAnimations ; 1413return genAnimatorConfig ; 1414} 1415 1416private static GeneratedAnimatorConfig 1417RemoveOffAnimationsFromOverrideLayers ( GeneratedAnimatorConfig config ) { 1418for ( int i = 1 ; i < config . layers . Count ; i ++ ) { 1419var layer = config . layers [ i ]; 1420layer . directBlendTree . entries . RemoveAll ( entry=> entry . name . EndsWith ( "_Off" )); 1421} 1422return config ; 1423} 1424 1425private static GeneratedAnimatorConfig 1426RemoveUnusedAnimations ( GeneratedAnimatorConfig config ) { 1427HashSet < string > referencedAnimations = new HashSet < string > (); 1428foreach ( var layer in config . layers ) { 1429foreach ( var entry in layer . directBlendTree . entries ) 1430referencedAnimations . Add ( entry . name ); 1431} 1432 1433config . animations = config . animations 1434. Where ( anim=> referencedAnimations . Contains ( anim . name )) 1435. ToList (); 1436 1437return config ; 1438} 1439 1440private static VRCExpressionsMenu GetOrCreateSubmenu ( 1441VRCExpressionsMenu parentMenu , 1442string submenuName ) { 1443// Check if submenu already exists 1444foreach ( var control in parentMenu . controls ) { 1445if ( control . type == VRCExpressionsMenu . Control . ControlType . SubMenu && 1446control . name == submenuName && control . subMenu != null ) { 1447return control . subMenu ; 1448} 1449} 1450 1451// Create new submenu 1452var newSubmenu = ScriptableObject . CreateInstance < VRCExpressionsMenu > (); 1453newSubmenu . name = submenuName ; 1454newSubmenu . controls = new List < VRCExpressionsMenu . Control > (); 1455 1456var newControl = new VRCExpressionsMenu . Control { 1457name = submenuName , 1458type = VRCExpressionsMenu . Control . ControlType . SubMenu , 1459subMenu = newSubmenu 1460}; 1461parentMenu . controls . Add ( newControl ); 1462 1463return newSubmenu ; 1464} 1465 1466private static void GenerateVRChatAssets ( 1467List < ToggleSpec > toggleSpecs , 1468VRCExpressionParameters vrcParams , 1469VRCExpressionsMenu vrcMenu 1470) { 1471var uniqueToggles = toggleSpecs 1472. Where ( t=> t . GetParameterName () != "YOTS_Weight" ) 1473. GroupBy ( t=> t . GetParameterName ()) 1474. Select ( g=> g . First ()) 1475. ToList (); 1476 1477// Update parameters 1478var paramList = new List < VRCExpressionParameters . Parameter > (); 1479paramList . AddRange ( vrcParams . parameters . Where ( p=> ! uniqueToggles . Any ( t=> t . GetParameterName () == p . name ))); 1480foreach ( var toggle in uniqueToggles ) { 1481string paramName = toggle . GetParameterName (); 1482paramList . Add ( new VRCExpressionParameters . Parameter { 1483name = paramName , 1484valueType = toggle . type == "radial" ? VRCExpressionParameters . ValueType . Float : VRCExpressionParameters . ValueType . Bool , 1485defaultValue = toggle . defaultValue , 1486saved = toggle . saved , 1487networkSynced = toggle . synced 1488}); 1489} 1490vrcParams . parameters = paramList . ToArray (); 1491 1492// Add toggles to menu (skipping those with disableMenuEntry=true) 1493foreach ( var toggle in toggleSpecs ) { 1494// Skip creating menu entries for toggles with disableMenuEntry=true 1495if ( toggle . disableMenuEntry ) 1496continue ; 1497 1498VRCExpressionsMenu currentMenu = vrcMenu ; 1499 1500// Navigate or create menu path if specified 1501if ( ! string . IsNullOrEmpty ( toggle . menuPath )) { 1502string trimmedPath = toggle . menuPath . Trim ( '/' ); 1503if ( ! string . IsNullOrEmpty ( trimmedPath )) { 1504var sections = trimmedPath . Split ( '/' ); 1505foreach ( var section in sections ) { 1506currentMenu = GetOrCreateSubmenu ( currentMenu , section ); 1507} 1508} 1509} 1510 1511// Add toggle control - use toggle.name for display but paramName for the parameter 1512string paramName = toggle . GetParameterName (); 1513if ( toggle . type == "radial" ) { 1514currentMenu . controls . Add ( new VRCExpressionsMenu . Control { 1515name = toggle . name , 1516type = VRCExpressionsMenu . Control . ControlType . RadialPuppet , 1517subParameters = new VRCExpressionsMenu . Control . Parameter []{ 1518new VRCExpressionsMenu . Control . Parameter { name = paramName } 1519} 1520}); 1521} else { 1522currentMenu . controls . Add ( new VRCExpressionsMenu . Control { 1523name = toggle . name , 1524type = VRCExpressionsMenu . Control . ControlType . Toggle , 1525parameter = new VRCExpressionsMenu . Control . Parameter { name = paramName }, 1526value = 1f 1527}); 1528} 1529} 1530} 1531 1532private static AnimationClip MirrorAnimationClipInMemory ( AnimationClip sourceClip ) { 1533if ( sourceClip == null ) { 1534Debug . LogError ( "Cannot mirror a null AnimationClip." ); 1535return null ; 1536} 1537 1538// Create a new clip instance in memory 1539AnimationClip mirroredClip = new AnimationClip (); 1540// Set a base name; the calling code will set a more specific final name 1541mirroredClip . name = sourceClip . name + "_Mirrored_InMemory" ; 1542 1543EditorCurveBinding [] bindings = AnimationUtility . GetCurveBindings ( sourceClip ); 1544 1545foreach ( var binding in bindings ) { 1546// Curves are value types (structs), copying them is fine. 1547AnimationCurve curve = AnimationUtility . GetEditorCurve ( sourceClip , binding ); 1548if ( curve == null ) continue ; 1549 1550EditorCurveBinding mirroredBinding = binding ; // Start with original 1551 1552// 1. Mirror Path 1553string mirroredPath = binding . path ; 1554mirroredPath = Regex . Replace ( mirroredPath , @"\bLeft\b" , "TEMP_RIGHT_MARKER" ); 1555mirroredPath = Regex . Replace ( mirroredPath , @"\bRight\b" , "Left" ); 1556mirroredPath = mirroredPath . Replace ( "TEMP_RIGHT_MARKER" , "Right" ); 1557 1558mirroredPath = Regex . Replace ( mirroredPath , @"\.L\b" , ".TEMP_R_MARKER" ); 1559mirroredPath = Regex . Replace ( mirroredPath , @"\.R\b" , ".L" ); 1560mirroredPath = mirroredPath . Replace ( ".TEMP_R_MARKER" , ".R" ); 1561 1562mirroredPath = Regex . Replace ( mirroredPath , @"_L\b" , "_TEMP_R_MARKER" ); 1563mirroredPath = Regex . Replace ( mirroredPath , @"_R\b" , "_L" ); 1564mirroredPath = mirroredPath . Replace ( "_TEMP_R_MARKER" , "_R" ); 1565 1566mirroredBinding . path = mirroredPath ; 1567 1568// 2. Mirror Property Name 1569string mirroredPropertyName = binding . propertyName ; 1570mirroredPropertyName = Regex . Replace ( mirroredPropertyName , @"\bLeft\b" , "TEMP_RIGHT_MARKER" ); 1571mirroredPropertyName = Regex . Replace ( mirroredPropertyName , @"\bRight\b" , "Left" ); 1572mirroredPropertyName = mirroredPropertyName . Replace ( "TEMP_RIGHT_MARKER" , "Right" ); 1573mirroredBinding . propertyName = mirroredPropertyName ; 1574 1575Debug . Log ( $"Saw binding: { binding . path } // { binding . propertyName } " ); 1576 1577// 3. Mirror Curve Values 1578bool valueNeedsNegating = false ; 1579if ( binding . propertyName == "m_LocalPosition.x" ) valueNeedsNegating = true ; 1580if ( binding . propertyName == "m_LocalRotation.y" || binding . propertyName == "m_LocalRotation.z" ) valueNeedsNegating = true ; 1581if ( binding . propertyName == "localEulerAnglesRaw.y" || binding . propertyName == "localEulerAnglesRaw.z" ) valueNeedsNegating = true ; 1582if ( binding . propertyName == "m_LocalScale.x" ) valueNeedsNegating = true ; 1583 1584if ( valueNeedsNegating ) { 1585Keyframe [] keys = curve . keys ; 1586for ( int i = 0 ; i < keys . Length ; i ++ ) { 1587keys [ i ]. value *= - 1f ; 1588keys [ i ]. inTangent *= - 1f ; 1589keys [ i ]. outTangent *= - 1f ; 1590} 1591// Create a new curve with modified keys, as AnimationCurve is a class but behaves like a value type here. 1592curve = new AnimationCurve ( keys ); 1593} 1594 1595// Set the potentially modified curve on the mirrored clip 1596AnimationUtility . SetEditorCurve ( mirroredClip , mirroredBinding , curve ); 1597} 1598 1599// Return the clip object without saving it 1600return mirroredClip ; 1601} 1602} 1603} 1604 1605#endif// UNITY_EDITOR