yum-slop/YOTS

an optimized toggle system for vrchat

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

yumUpdate READMEcc376eb

master
15.7 KiB552 linesraw

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]

Unity should look like this.

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:

  1. Add more toggles.
  2. Add blendshape toggles in addition to mesh toggles.
  3. Declare a dependency on another toggle with "dependencies": ["someOtherToggle"].
  4. Install a toggle at a custom path with "menuPath": "/my/custom/path".
  5. Add a radial puppet with "type": "radial".
  6. Use "defaultValue": 0.0 to set a toggle to off by default.

Toggle options are documented in two places:

  1. The ToggleSpec definition at the top of YOTSCore.cs.
  2. 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 synced and saved flags.
  • 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.

Motivation

Animators have a two fundamental problems:

  1. Layers are extremely slow. Their runtime scales with O(n^2).
  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:

  1. An animation file for the toggle (usually two)
  2. The avatar menu
  3. The avatar parameters
  4. 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:

  1. Adding new articles of clothing. You now have to edit all your existing avatar-wide animations to include them.
  2. Adding new toggles or sliders. You may want them to affect a large set of items. For example: you added a minimum brightness slider, and now have to animate 20 different articles of clothing.
  3. Removing articles of clothing. You should remove them from your avatar-wide animations. (No one does this because it's a pain in the ass!)
  4. Removing toggles or sliders. It's easy to accidentally orphan an animator layer, or a parameter somewhere.

These are all vastly easier to perform through 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![Unity should look like this.](./Images/setup_picture_00.PNG)
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
75  menu "/Clothes", the parameter is called "/Clothes/Shirt".
76* The generated animator has exactly as many layers as the maximum length of
77  any dependency chain in your dependency graph.
78* The generated animator's parameters and layers are simply appended to your
79  input animator.
80* Each layer consists of a single direct blendtree (DBT) with write defaults
81  on.
82  * Write defaults [behave uniquely on DBTs.](https://vrc.school/docs/Other/DBT-Combining#af9b08d723fb47698407a8c0dde577dc).
83    They 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
111   avatar-wide animations to include them.
1122. Adding new toggles or sliders. You may want them to affect a large
113   set of items. For example: you added a minimum brightness slider, and now
114   have to animate 20 different articles of clothing.
1153. Removing articles of clothing. You should remove them from your avatar-wide
116   animations. (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
118   layer, 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
205  Error: ToggleSpec $A and $B both animate the same property $PROPERTY. Declare
206  a 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
211  Error: Cycle detected: ToggleSpecs $ALL\_AFFECTED\_TOGGLES have a cyclic
212  dependency. Delete one Dependency attribute in the chain to resolve the
213  conflict.
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