yum-slop/YOTS
an optimized toggle system for vrchat
git clone https://git.yummers.dev/yum-slop/YOTS
cc376eb
master
YOTS: yum's optimized toggle system
YOTS is a text-based system for managing VRChat toggles and sliders. It translates a single config file into VRChat menus, parameters, animations, and animator layers. They are added to your avatar non-destructively.
YOTS is efficient, maintainable, non-destructive, and compatible with any existing animator.
yum!
Installation
First, install Modular Avatar.
Then grab the latest release from the releases page.
Creating your first toggle
Open your text editor of choice and paste this in:
{
"api_version" : "1.0" ,
"toggles" : [
{
"name" : "Shirt" ,
"meshToggles" : ["Shirt" ]
}
]
}
Feel free to replace "Shirt" with the name of some other mesh on your avatar.
Save it to Assets/animator.json. [1]
Drag Assets/yum_food/YOTS.prefab anywhere on your avatar. [2]
Select it in the hierarchy, and drag Assets/animator.json onto the "Json Config" field. [3]
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 should toggle off.
Congratulations!
A logical sequence of things to try:
- Add more toggles.
- Add blendshape toggles in addition to mesh toggles.
- Declare a dependency on another toggle with
"dependencies": ["someOtherToggle"]. - Install a toggle at a custom path with
"menuPath": "/my/custom/path". - Add a radial puppet with
"type": "radial". - Use
"defaultValue": 0.0to set a toggle to off by default.
Toggle options are documented in two places:
- The ToggleSpec definition at the top of YOTSCore.cs.
- The Examples.
Technical details
- Toggles are created as boolean parameters.
- Radial puppets (sliders) are created as float parameters.
- All parameters are synced and saved by default.
- You can override this with the
syncedandsavedflags.
- You can override this with the
- YOTS will not stop you from over-filling menus.
- Auto-splitting menus is not yet implemented.
- The generated VRChat parameter and animator parameters are simply "menuPath/name". For example, if you create a toggle called "Shirt" in the menu "/Clothes", the parameter is called "/Clothes/Shirt".
- The generated animator has exactly as many layers as the maximum length of any dependency chain in your dependency graph.
- The generated animator's parameters and layers are simply appended to your input animator.
- Each layer consists of a single direct blendtree (DBT) with write defaults
on.
- Write defaults behave uniquely on DBTs.. They do not overwrite layers that come before them.
Motivation
Animators have a two fundamental problems:
- Layers are extremely slow. Their runtime scales with O(n^2).
- Animations which affect the same thing cannot be in the same layer.
Any efficient animator must minimize the number of layers. Any real-world animator requires multiple layers because some toggles will need to override other toggles. Thus the goal is to provide this overriding capability while still minimizing the number of layers.
In addition, whenever you add a toggle to a VRChat avatar, you need to edit at least 4 files:
- An animation file for the toggle (usually two)
- The avatar menu
- The avatar parameters
- The animator
With YOTS, you only have to edit one file.
Finally, there are many unduly tedious tasks which you wind up performing over the lifetime of an avatar:
- Adding new articles of clothing. You now have to edit all your existing avatar-wide animations to include them.
- 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.
- Removing articles of clothing. You should remove them from your avatar-wide animations. (No one does this because it's a pain in the ass!)
- 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 YOTS.
Design derivation
Consider this basic example: you have a shirt and a jacket. The shirt hides the chest to avoid clipping. The jacket hides the shirt sleeves to hide clipping:
- [ToggleSpec] Shirt - [MeshToggle] Shirt - [BlendShape] Chest hidden - [ToggleSpec] Jacket - [MeshToggle] Jacket - [BlendShape] Shirt sleeves hidden
A system could trivially be made to generate animations for this:
- [Animation] ShirtOn - [MeshToggle] Shirt on - [BlendShape] Chest hide -> 100 - [Animation] ShirtOff - [MeshToggle] Shirt off - [BlendShape] Chest hide -> 0 - [Animation] JacketOn - [MeshToggle] Jacket on - [BlendShape] Shirt sleeves hide -> 100 - [Animation] JacketOff - [MeshToggle] Jacket off - [BlendShape] Shirt sleeves hide -> 0
This system works perfectly as written and can be trivially implemented by driving all 4 animations in a single layer with one DBT. Problems arise when you have two assets that want to animate the same blendshape. In that case, you must declare a dependency. In our example, suppose we wanted to add an undershirt. It also wants to hide the chest:
- [ToggleSpec] Undershirt - [MeshToggle] Undershirt - [BlendShape] Chest hidden
The animations are also trivial:
- [Animation] UndershirtOn - [MeshToggle] Undershirt on - [BlendShape] Chest hide -> 100 - [Animation] UndershirtOff - [MeshToggle] Undershirt Off - [BlendShape] Chest hide -> 0
The problem is that since Undershirt{On,Off} and Shirt{On,Off} both animate the "Chest hide" blendshape, you cannot put them into the same DBT. It's even worse than that: if you split them into layers, such that the undershirt is evaluated before the shirt, then if the shirt is toggled off, it will always set the "Chest hide" blendshape to 0. With the shirt off and undershirt on, the chest will clip through the undershirt.
To fix this, we can declare a dependency. In this case the order doesn't matter, so I will just use the convention that outer layers of garments depend on inner layers.
- [ToggleSpec] Undershirt - [MeshToggle] Undershirt - [BlendShape] Chest hidden - [ToggleSpec] Shirt - [Dependency] Undershirt - [MeshToggle] Shirt - [BlendShape] Chest hidden
This situation can be detected robustly. We simply do a topological sort of all ToggleSpec nodes according to their declared dependencies. This will give us a set of directed acyclic graphs (a forest). We can maintain a set of attributes affected by ToggleSpec nodes while iterating through them. Any two nodes which affect the same attribute must be in the same DAG and not at the same level. This can be surfaced to the user as a critical error. It can tell them something like:
Error: ToggleSpec $A and $B both animate the same property $PROPERTY. Declare a dependency to resolve the conflict.
We can also detect cycles in the graph (which wouldn't be possible to implement in the animator anyway!) and report that to the user:
Error: Cycle detected: ToggleSpecs $ALL_AFFECTED_TOGGLES have a cyclic dependency. Delete one Dependency attribute in the chain to resolve the conflict.
The forest of DAGs is then used to generate the animator. To generate it, you iterate a total of n times, where n is the largest depth of any DAG in the forest. Each layer in the animator contains every ToggleSpec of depth k, of any DAG in the forest. For example, a forest with 1000 separate DAGs of maximum depth 3 would only generate a 3-layer animator. A forest with one DAG of depth 300 would generate a 300 layer animator. The maximum length of the DAG characterizes the number of nodes in the animator.
There are two types of layers: the first layer, and every other layer. For the first layer, because it's free to overwrite anything on the avatar, the DBT can be constructed of pairs of Thing{On,Off} animations. Because of our topological sort, we know that these nodes are all independent, so no two pairs are animating the same thing. The config parser would have errored out by now if that was the case.
The successive layers are comprised of ToggleSpecs which animate one or more attributes. At least one of these attributes is already being animated. We must split the node into two parts. One part (the independent part) consists entirely of attributes which are not already animated. The other part consists entirely of nodes which are being animated (the dependent part). The independent part may be comprised of the empty set, in which case it is discarded. If it's not empty, it's added to the first layer's DBT. The dependent part is guaranteed to be non-empty, and is simply added to a new DBT.
Specification language
We use JSON to represent the specification. The example above is expressed as follows:
{
"api_version" : "1.0" ,
"toggles" : [
{
"name" : "Undershirt" ,
"meshToggles" : ["Undershirt" ],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide"
}
]
},
{
"name" : "Shirt" ,
"dependencies" : ["Undershirt" ],
"meshToggles" : ["Shirt" ],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide"
}
]
},
{
"name" : "Jacket" ,
"meshToggles" : ["Jacket" ],
"blendShapes" : [
{
"path" : "Shirt" ,
"blendShape" : "Sleeves_Hide"
}
]
}
]
}
Given that config, we would run the described topological sort, erroring out if there are unconnected nodes which affect the same attribute, or if there is a cycle.
In the topological sort of the dependency graph, we have Undershirt and Jacket running on the first layer, and Shirt running on the second layer.
Given that dependency graph, let's consider how we'd generate the animations. The first layer's animations are trivial:
{
"animations" : [
// Shirt
{
"name" : "Shirt_On" ,
"meshToggles" : [
{
"path" : "Shirt" ,
"value" : 1.0
}
],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide" ,
"value" : 1.0
}
]
},
{
"name" : "Shirt_Off" ,
"meshToggles" : [
{
"path" : "Shirt" ,
"value" : 0.0
}
],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide" ,
"value" : 0.0
}
]
},
// Jacket
{
"name" : "Jacket_On" ,
"meshToggles" : [
{
"path" : "Jacket" ,
"value" : 1.0
}
],
"blendShapes" : [
{
"path" : "Shirt" ,
"blendShape" : "Sleeves_Hide" ,
"value" : 1.0
}
]
},
{
"name" : "Jacket_Off" ,
"meshToggles" : [
{
"path" : "Jacket" ,
"value" : 0.0
}
],
"blendShapes" : [
{
"path" : "Shirt" ,
"blendShape" : "Sleeves_Hide" ,
"value" : 0.0
}
]
}
]
}
Naively, we might expect the second animations to be this:
{
"animations" : [
{
"name" : "Undershirt_On" ,
"meshToggles" : [
{
"path" : "Undershirt" ,
"value" : 1.0
}
],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide" ,
"value" : 0.0
}
]
},
{
"name" : "Undershirt_Off" ,
"meshToggles" : [
{
"path" : "Undershirt" ,
"value" : 0.0
}
],
"blendShapes" : [
{
"path" : "Body" ,
"blendShape" : "Chest_Hide" ,
"value" : 1.0
}
]
}
]
}
However, we must split the UnderShirt animations into the independent and dependent parts:
// Independent part {"animations" : [ {"name" :"Undershirt_On_Independent" ,"meshToggles" : [ {"path" :"Undershirt" ,"value" :1.0 } ], } {"name" :"Undershirt_Off_Independent" ,"meshToggles" : [ {"path" :"Undershirt" ,"value" :0.0 } ] } ] }
// Dependent part {"animations" : [ {"name" :"Undershirt_On_Dependent" ,"blendShapes" : [ {"path" :"Body" ,"blendShape" :"Chest_Hide" ,"value" :1.0 } ] } ] }
Then we'd append the independent part to the first layer's animations. We could then report this in a nice object:
{
"animationLayers" : [
// Layer 1
{
"animations" : [
{ "name" : "Shirt_On" , ... },
{ "name" : "Shirt_Off" , ... },
{ "name" : "Jacket_On" , ... },
{ "name" : "Jacket_Off" , ... },
{ "name" : "Undershirt_On_Independent" , ... },
{ "name" : "Undershirt_Off_Independent" , ... }
]
},
// Layer 2
{
"animations" : [
{ "name" : "Undershirt_On_Dependent" , ... },
{ "name" : "Undershirt_Off_Dependent" , ... }
]
}
]
}
We will also need toggles for material properties. These are discussed in Extension 2.
Our animations are complete. Our animator can trivially use the names of each ToggleSpec as its parameters. To actually generate the first layer, we'll use Unity's built in APIs.
// TODO document this part. For now just look at YOTSCore.cs.
We have to generate animations, configs for debug purposes, and finally an animator file.
// TODO document this. Again just look at YOTSCore.cs.
Extensions
1. Order-agnostic dependency
For ease of use, a subtype of the [Dependency] attribute called [OrderAgnosticDependency] may be created. Its function is to allow the runtime to create an arbitrary ordering whenever two ToggleSpecs try to affect the same node. For the initial version, only an explicit [Dependency] is created.
2. GameObject material animation resolution
Animations affecting material properties necessarily animate the same property on all materials on the same GameObject. The situation can be detected by iterating all GameObjects on the avatar. For each skinned mesh renderer, we can check which materials exist. Any time a (gameobject,materials) pair is animated, we must generate animations for (gameobject,neighbor_material) for every neighboring material on that gameobject. These generated animations should be logged during generation.
3. GUI frontend
There's no reason why we can't make a GUI for generating and modifying the config file. This would be appealing to some users.
4. Prefab support / modularization
Right now the tool is designed to have one config per avatar. It may be useful to have one config per modular system. Moreover, users may wish to "bake" out the generated animator/menu/params/animations and distribute those with prefabs. Some support for that would be useful.
(Partially done - prefabs are done, but baking out is not.)
5. Automatic dependency declarations
Artists declaring cross-module dependencies is not practical. They'd have to agree to a standard, and there are obvious conventions (inside out, outside in) which conflict with each other. Might require linear programming?
5.1. User-facing dependency resolution
When two toggles drive the same attribute (shapekey, shader, etc.) without a dependency, this should be surfaced to the user, since only they can know which one is better.
A visual example would go a long way.
6. Artist facing debug tooling
Need GUI ways to handle common debug scenarios. For example, enumerate shapekeys driven by multiple toggles.
7. Support heterogeneous avatars
VRCFury drives base shapekey; YOTS must also drive it. How does this support it?
1# YOTS: yum's optimized toggle system 2 3YOTS is a text-based system for managing VRChat toggles and sliders. It 4translates a single config file into VRChat menus, parameters, animations, and 5animator layers. They are added to your avatar non-destructively. 6 7YOTS is efficient, maintainable, non-destructive, and compatible with any 8existing animator. 9 10yum! 11 12## Installation 13 14First, install [Modular Avatar](https://modular-avatar.nadena.dev/). 15 16Then grab the latest release from [the releases page](https://github.com/yum-food/YOTS/releases/latest). 17 18## Creating your first toggle 19 20Open your text editor of choice and paste this in: 21 22``` json 23{ 24"api_version" : "1.0" , 25"toggles" : [ 26{ 27"name" : "Shirt" , 28"meshToggles" : [ "Shirt" ] 29} 30] 31} 32``` 33 34Feel free to replace "Shirt" with the name of some other mesh on your avatar. 35 36Save it to Assets/animator.json. [1] 37 38Drag Assets/yum\_food/YOTS.prefab anywhere on your avatar. [2] 39 40Select it in the hierarchy, and drag Assets/animator.json onto the "Json Config" field. [3] 41 42 43 44Enter play mode. Enable an emulator (I use Lyuma's av3emulator). Open your 45menu. You should see a YOTS submenu. Click it, then click Shirt. Your shirt 46should toggle off. 47 48Congratulations! 49 50A logical sequence of things to try: 51 521. Add more toggles. 532. Add blendshape toggles in addition to mesh toggles. 543. Declare a dependency on another toggle with `"dependencies": ["someOtherToggle"]`. 554. Install a toggle at a custom path with `"menuPath": "/my/custom/path"`. 565. Add a radial puppet with `"type": "radial"`. 576. Use `"defaultValue": 0.0` to set a toggle to off by default. 58 59Toggle options are documented in two places: 60 611. The ToggleSpec definition at the top of 62[YOTSCore.cs](./Scripts/YOTSCore.cs). 632. The [Examples](./Examples). 64 65## Technical details 66 67* Toggles are created as boolean parameters. 68* Radial puppets (sliders) are created as float parameters. 69* All parameters are synced and saved by default. 70* You can override this with the `synced` and `saved` flags. 71* YOTS will not stop you from over-filling menus. 72* Auto-splitting menus is not yet implemented. 73* The generated VRChat parameter and animator parameters are simply 74"menuPath/name". For example, if you create a toggle called "Shirt" in the 75menu "/Clothes", the parameter is called "/Clothes/Shirt". 76* The generated animator has exactly as many layers as the maximum length of 77any dependency chain in your dependency graph. 78* The generated animator's parameters and layers are simply appended to your 79input animator. 80* Each layer consists of a single direct blendtree (DBT) with write defaults 81on. 82* Write defaults [behave uniquely on DBTs.](https://vrc.school/docs/Other/DBT-Combining#af9b08d723fb47698407a8c0dde577dc). 83They do not overwrite layers that come before them. 84 85## Motivation 86 87Animators have a two fundamental problems: 88 891. Layers are extremely slow. Their runtime scales with O(n^2). 902. Animations which affect the same thing cannot be in the same layer. 91 92Any efficient animator must minimize the number of layers. Any real-world 93animator requires multiple layers because some toggles will need to override 94other toggles. Thus the goal is to provide this overriding capability while 95still minimizing the number of layers. 96 97In addition, whenever you add a toggle to a VRChat avatar, you need to edit at 98least 4 files: 99 1001. An animation file for the toggle (usually two) 1012. The avatar menu 1023. The avatar parameters 1034. The animator 104 105With YOTS, you only have to edit one file. 106 107Finally, there are many unduly tedious tasks which you wind up performing over 108the lifetime of an avatar: 109 1101. Adding new articles of clothing. You now have to edit all your existing 111avatar-wide animations to include them. 1122. Adding new toggles or sliders. You may want them to affect a large 113set of items. For example: you added a minimum brightness slider, and now 114have to animate 20 different articles of clothing. 1153. Removing articles of clothing. You should remove them from your avatar-wide 116animations. (No one does this because it's a pain in the ass!) 1174. Removing toggles or sliders. It's easy to accidentally orphan an animator 118layer, or a parameter somewhere. 119 120These are all vastly easier to perform through YOTS. 121 122## Design derivation 123 124Consider this basic example: you have a shirt and a jacket. The shirt hides the 125chest to avoid clipping. The jacket hides the shirt sleeves to hide clipping: 126 127``` 128- [ToggleSpec] Shirt 129- [MeshToggle] Shirt 130- [BlendShape] Chest hidden 131- [ToggleSpec] Jacket 132- [MeshToggle] Jacket 133- [BlendShape] Shirt sleeves hidden 134``` 135 136A system could trivially be made to generate animations for this: 137 138``` 139- [Animation] ShirtOn 140- [MeshToggle] Shirt on 141- [BlendShape] Chest hide -> 100 142- [Animation] ShirtOff 143- [MeshToggle] Shirt off 144- [BlendShape] Chest hide -> 0 145- [Animation] JacketOn 146- [MeshToggle] Jacket on 147- [BlendShape] Shirt sleeves hide -> 100 148- [Animation] JacketOff 149- [MeshToggle] Jacket off 150- [BlendShape] Shirt sleeves hide -> 0 151``` 152 153This system works perfectly as written and can be trivially implemented by 154driving all 4 animations in a single layer with one DBT. 155Problems arise when you have two assets that want to animate the same 156blendshape. In that case, you must declare a dependency. In our example, 157suppose we wanted to add an undershirt. It also wants to hide the chest: 158 159``` 160- [ToggleSpec] Undershirt 161- [MeshToggle] Undershirt 162- [BlendShape] Chest hidden 163``` 164 165The animations are also trivial: 166 167``` 168- [Animation] UndershirtOn 169- [MeshToggle] Undershirt on 170- [BlendShape] Chest hide -> 100 171- [Animation] UndershirtOff 172- [MeshToggle] Undershirt Off 173- [BlendShape] Chest hide -> 0 174``` 175 176The problem is that since Undershirt{On,Off} and Shirt{On,Off} both animate the 177"Chest hide" blendshape, you cannot put them into the same DBT. It's even worse 178than that: if you split them into layers, such that the undershirt is evaluated 179before the shirt, then if the shirt is toggled off, it will always set the 180"Chest hide" blendshape to 0. With the shirt off and undershirt on, the chest 181will clip through the undershirt. 182 183To fix this, we can declare a *dependency*. In this case the order doesn't 184matter, so I will just use the convention that outer layers of garments depend 185on inner layers. 186 187``` 188- [ToggleSpec] Undershirt 189- [MeshToggle] Undershirt 190- [BlendShape] Chest hidden 191- [ToggleSpec] Shirt 192- [Dependency] Undershirt 193- [MeshToggle] Shirt 194- [BlendShape] Chest hidden 195``` 196 197This situation can be detected robustly. We simply do a topological sort of all 198ToggleSpec nodes according to their declared dependencies. This will give us a 199set of directed acyclic graphs (a forest). We can maintain a set of attributes 200affected by ToggleSpec nodes while iterating through them. **Any two nodes 201which affect the same attribute must be in the same DAG and not at the same 202level.** This can be surfaced to the user as a critical error. It can tell them 203something like: 204 205Error: ToggleSpec $A and $B both animate the same property $PROPERTY. Declare 206a dependency to resolve the conflict. 207 208We can also detect cycles in the graph (which wouldn't be possible to implement 209in the animator anyway!) and report that to the user: 210 211Error: Cycle detected: ToggleSpecs $ALL\_AFFECTED\_TOGGLES have a cyclic 212dependency. Delete one Dependency attribute in the chain to resolve the 213conflict. 214 215The forest of DAGs is then used to generate the animator. To generate it, you 216iterate a total of n times, where n is the largest depth of any DAG in the 217forest. **Each layer in the animator contains every ToggleSpec of depth k, of 218any DAG in the forest.** For example, a forest with 1000 separate DAGs of 219maximum depth 3 would only generate a 3-layer animator. A forest with one DAG 220of depth 300 would generate a 300 layer animator. The maximum length of the DAG 221characterizes the number of nodes in the animator. 222 223There are two types of layers: the first layer, and every other layer. For the 224first layer, because it's free to overwrite anything on the avatar, the DBT can 225be constructed of pairs of Thing{On,Off} animations. Because of our topological 226sort, we know that these nodes are all independent, so no two pairs are 227animating the same thing. The config parser would have errored out by now if 228that was the case. 229 230The successive layers are comprised of ToggleSpecs which animate one or more 231attributes. At least one of these attributes is already being animated. We must 232split the node into two parts. One part (the independent part) consists 233entirely of attributes which are not already animated. The other part consists 234entirely of nodes which are being animated (the dependent part). The 235independent part may be comprised of the empty set, in which case it is 236discarded. If it's not empty, it's added to the first layer's DBT. The 237dependent part is guaranteed to be non-empty, and is simply added to a new DBT. 238 239### Specification language 240 241We use JSON to represent the specification. The example above is expressed as 242follows: 243 244``` json 245{ 246"api_version" : "1.0" , 247"toggles" : [ 248{ 249"name" : "Undershirt" , 250"meshToggles" : [ "Undershirt" ], 251"blendShapes" : [ 252{ 253"path" : "Body" , 254"blendShape" : "Chest_Hide" 255} 256] 257}, 258{ 259"name" : "Shirt" , 260"dependencies" : [ "Undershirt" ], 261"meshToggles" : [ "Shirt" ], 262"blendShapes" : [ 263{ 264"path" : "Body" , 265"blendShape" : "Chest_Hide" 266} 267] 268}, 269{ 270"name" : "Jacket" , 271"meshToggles" : [ "Jacket" ], 272"blendShapes" : [ 273{ 274"path" : "Shirt" , 275"blendShape" : "Sleeves_Hide" 276} 277] 278} 279] 280} 281``` 282 283Given that config, we would run the described topological sort, erroring out if 284there are unconnected nodes which affect the same attribute, or if there is a 285cycle. 286 287In the topological sort of the dependency graph, we have Undershirt and Jacket 288running on the first layer, and Shirt running on the second layer. 289 290Given that dependency graph, let's consider how we'd generate the animations. 291The first layer's animations are trivial: 292 293``` json 294{ 295"animations" : [ 296// Shirt 297{ 298"name" : "Shirt_On" , 299"meshToggles" : [ 300{ 301"path" : "Shirt" , 302"value" : 1.0 303} 304], 305"blendShapes" : [ 306{ 307"path" : "Body" , 308"blendShape" : "Chest_Hide" , 309"value" : 1.0 310} 311] 312}, 313{ 314"name" : "Shirt_Off" , 315"meshToggles" : [ 316{ 317"path" : "Shirt" , 318"value" : 0.0 319} 320], 321"blendShapes" : [ 322{ 323"path" : "Body" , 324"blendShape" : "Chest_Hide" , 325"value" : 0.0 326} 327] 328}, 329// Jacket 330{ 331"name" : "Jacket_On" , 332"meshToggles" : [ 333{ 334"path" : "Jacket" , 335"value" : 1.0 336} 337], 338"blendShapes" : [ 339{ 340"path" : "Shirt" , 341"blendShape" : "Sleeves_Hide" , 342"value" : 1.0 343} 344] 345}, 346{ 347"name" : "Jacket_Off" , 348"meshToggles" : [ 349{ 350"path" : "Jacket" , 351"value" : 0.0 352} 353], 354"blendShapes" : [ 355{ 356"path" : "Shirt" , 357"blendShape" : "Sleeves_Hide" , 358"value" : 0.0 359} 360] 361} 362] 363} 364``` 365 366Naively, we might expect the second animations to be this: 367 368``` json 369{ 370"animations" : [ 371{ 372"name" : "Undershirt_On" , 373"meshToggles" : [ 374{ 375"path" : "Undershirt" , 376"value" : 1.0 377} 378], 379"blendShapes" : [ 380{ 381"path" : "Body" , 382"blendShape" : "Chest_Hide" , 383"value" : 0.0 384} 385] 386}, 387{ 388"name" : "Undershirt_Off" , 389"meshToggles" : [ 390{ 391"path" : "Undershirt" , 392"value" : 0.0 393} 394], 395"blendShapes" : [ 396{ 397"path" : "Body" , 398"blendShape" : "Chest_Hide" , 399"value" : 1.0 400} 401] 402} 403] 404} 405``` 406 407However, we must split the UnderShirt animations into the independent and 408dependent parts: 409 410``` json 411// Independent part 412{ 413"animations" : [ 414{ 415"name" : "Undershirt_On_Independent" , 416"meshToggles" : [ 417{ 418"path" : "Undershirt" , 419"value" : 1.0 420} 421], 422} 423{ 424"name" : "Undershirt_Off_Independent" , 425"meshToggles" : [ 426{ 427"path" : "Undershirt" , 428"value" : 0.0 429} 430] 431} 432] 433} 434``` 435 436``` json 437// Dependent part 438{ 439"animations" : [ 440{ 441"name" : "Undershirt_On_Dependent" , 442"blendShapes" : [ 443{ 444"path" : "Body" , 445"blendShape" : "Chest_Hide" , 446"value" : 1.0 447} 448] 449} 450] 451} 452``` 453 454Then we'd append the independent part to the first layer's animations. We could 455then report this in a nice object: 456 457``` json 458{ 459"animationLayers" : [ 460// Layer 1 461{ 462"animations" : [ 463{ "name" : "Shirt_On" , ... }, 464{ "name" : "Shirt_Off" , ... }, 465{ "name" : "Jacket_On" , ... }, 466{ "name" : "Jacket_Off" , ... }, 467{ "name" : "Undershirt_On_Independent" , ... }, 468{ "name" : "Undershirt_Off_Independent" , ... } 469] 470}, 471// Layer 2 472{ 473"animations" : [ 474{ "name" : "Undershirt_On_Dependent" , ... }, 475{ "name" : "Undershirt_Off_Dependent" , ... } 476] 477} 478] 479} 480``` 481 482We will also need toggles for material properties. These are discussed in 483Extension 2. 484 485Our animations are complete. Our animator can trivially use the names of each 486ToggleSpec as its parameters. To actually generate the first layer, we'll use 487Unity's built in APIs. 488 489// TODO document this part. For now just look at YOTSCore.cs. 490 491We have to generate animations, configs for debug purposes, and finally an 492animator file. 493 494// TODO document this. Again just look at YOTSCore.cs. 495 496## Extensions 497 498### 1. Order-agnostic dependency 499 500For ease of use, a subtype of the [Dependency] attribute called 501[OrderAgnosticDependency] may be created. Its function is to allow the runtime 502to create an arbitrary ordering whenever two ToggleSpecs try to affect the same 503node. For the initial version, only an explicit [Dependency] is created. 504 505### 2. GameObject material animation resolution 506 507Animations affecting material properties necessarily animate the same property 508on all materials on the same GameObject. The situation can be detected by 509iterating all GameObjects on the avatar. For each skinned mesh renderer, we can 510check which materials exist. **Any time a (gameobject,materials) pair is 511animated, we must generate animations for (gameobject,neighbor_material) for 512every neighboring material on that gameobject.** These generated animations 513should be logged during generation. 514 515### 3. GUI frontend 516 517There's no reason why we can't make a GUI for generating and modifying the 518config file. This would be appealing to some users. 519 520### 4. Prefab support / modularization 521 522Right now the tool is designed to have one config per avatar. It may be useful 523to have one config per modular system. Moreover, users may wish to "bake" out 524the generated animator/menu/params/animations and distribute those with 525prefabs. Some support for that would be useful. 526 527(Partially done - prefabs are done, but baking out is not.) 528 529### 5. Automatic dependency declarations 530 531Artists declaring cross-module dependencies is not practical. They'd have to 532agree to a standard, and there are obvious conventions (inside out, outside in) 533which conflict with each other. Might require linear programming? 534 535### 5.1. User-facing dependency resolution 536 537When two toggles drive the same attribute (shapekey, shader, etc.) without a 538dependency, this should be surfaced to the user, since only they can know 539which one is better. 540 541A visual example would go a long way. 542 543### 6. Artist facing debug tooling 544 545Need GUI ways to handle common debug scenarios. For example, enumerate 546shapekeys driven by multiple toggles. 547 548### 7. Support heterogeneous avatars 549 550VRCFury drives base shapekey; YOTS must also drive it. How does this support 551it? 552