├── .gitignore
├── Media
├── setup.mp4
├── Preview.gif
├── Preview.png
└── Web
│ ├── Preview.webp
│ ├── PreviewGif.webp
│ ├── Preview.webp.meta
│ └── PreviewGif.webp.meta
├── Media.meta
├── README.md.meta
├── Editor
├── ControllerGenerationMethods.cs.meta
├── CustomObjectSyncCreator.cs.meta
├── VRLabs.CustomObjectSync.Editor.asmdef.meta
├── CustomObjectSyncCreatorWindow.cs.meta
├── VRLabs.CustomObjectSync.Editor.asmdef
├── CustomObjectSyncCreatorWindow.cs
├── ControllerGenerationMethods.cs
└── CustomObjectSyncCreator.cs
├── LICENSE.meta
├── package.json.meta
├── Resources
├── World.prefab.meta
├── Custom Object Sync.prefab.meta
└── World.prefab
├── Editor.meta
├── Resources.meta
├── package.json
├── LICENSE
├── .github
└── workflows
│ └── VRC-Asset-Release-And-Upload.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .github/.DS_Store
2 | .DS_Store
3 | /Media/*.meta
4 |
--------------------------------------------------------------------------------
/Media/setup.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/HEAD/Media/setup.mp4
--------------------------------------------------------------------------------
/Media/Preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/HEAD/Media/Preview.gif
--------------------------------------------------------------------------------
/Media/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/HEAD/Media/Preview.png
--------------------------------------------------------------------------------
/Media.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 551f146e56004d50abcfa909035699ef
3 | timeCreated: 1714670814
--------------------------------------------------------------------------------
/Media/Web/Preview.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/HEAD/Media/Web/Preview.webp
--------------------------------------------------------------------------------
/README.md.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 9a851e68ea284bea8e3cb975608bbe1e
3 | timeCreated: 1714671136
--------------------------------------------------------------------------------
/Media/Web/PreviewGif.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/HEAD/Media/Web/PreviewGif.webp
--------------------------------------------------------------------------------
/Editor/ControllerGenerationMethods.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 1ac587a32b2e4f6b8b078567ba21359e
3 | timeCreated: 1704890805
--------------------------------------------------------------------------------
/Editor/CustomObjectSyncCreator.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 35869b620c524f878a28a6fe04b09609
3 | timeCreated: 1704888691
--------------------------------------------------------------------------------
/LICENSE.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 42ae2f3f247ea65418d9fb4dfc41d92d
3 | DefaultImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/package.json.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 3f2e4b645fa1da64f873ae219f12ab4a
3 | TextScriptImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Media/Web/Preview.webp.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 8fdfc3e8de2f70b4da2702cb7ddaff25
3 | DefaultImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Media/Web/PreviewGif.webp.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 7057abf8c46c4e246b7be20d133cdafb
3 | DefaultImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Resources/World.prefab.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 321681d27af33d04680c0839c27d6a19
3 | PrefabImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Editor.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 9437062bf141d2647b0eee3beb86c4ba
3 | folderAsset: yes
4 | DefaultImporter:
5 | externalObjects: {}
6 | userData:
7 | assetBundleName:
8 | assetBundleVariant:
9 |
--------------------------------------------------------------------------------
/Resources.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: b9ab818bfe7dbcb41bc203fd95d19e0d
3 | folderAsset: yes
4 | DefaultImporter:
5 | externalObjects: {}
6 | userData:
7 | assetBundleName:
8 | assetBundleVariant:
9 |
--------------------------------------------------------------------------------
/Resources/Custom Object Sync.prefab.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: d51eb264fa89a5b4d9b95f344f169766
3 | PrefabImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Editor/VRLabs.CustomObjectSync.Editor.asmdef.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: c0e09ccbc2ed3564b9b844434974a6e9
3 | AssemblyDefinitionImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Editor/CustomObjectSyncCreatorWindow.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 7312e833f9f2d894da708b98ac0dca9b
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/Editor/VRLabs.CustomObjectSync.Editor.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "VRLabs.CustomObjectSync.Editor",
3 | "rootNamespace": "",
4 | "references": [],
5 | "includePlatforms": [
6 | "Editor"
7 | ],
8 | "excludePlatforms": [],
9 | "allowUnsafeCode": false,
10 | "overrideReferences": false,
11 | "precompiledReferences": [],
12 | "autoReferenced": true,
13 | "defineConstraints": [],
14 | "versionDefines": [],
15 | "noEngineReferences": false
16 | }
--------------------------------------------------------------------------------
/Resources/World.prefab:
--------------------------------------------------------------------------------
1 | %YAML 1.1
2 | %TAG !u! tag:unity3d.com,2011:
3 | --- !u!1 &1731590799922980152
4 | GameObject:
5 | m_ObjectHideFlags: 0
6 | m_CorrespondingSourceObject: {fileID: 0}
7 | m_PrefabInstance: {fileID: 0}
8 | m_PrefabAsset: {fileID: 0}
9 | serializedVersion: 6
10 | m_Component:
11 | - component: {fileID: 3937263297648365828}
12 | m_Layer: 0
13 | m_Name: World
14 | m_TagString: Untagged
15 | m_Icon: {fileID: 0}
16 | m_NavMeshLayer: 0
17 | m_StaticEditorFlags: 0
18 | m_IsActive: 1
19 | --- !u!4 &3937263297648365828
20 | Transform:
21 | m_ObjectHideFlags: 0
22 | m_CorrespondingSourceObject: {fileID: 0}
23 | m_PrefabInstance: {fileID: 0}
24 | m_PrefabAsset: {fileID: 0}
25 | m_GameObject: {fileID: 1731590799922980152}
26 | serializedVersion: 2
27 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
28 | m_LocalPosition: {x: 0, y: 0, z: 0}
29 | m_LocalScale: {x: 1, y: 1, z: 1}
30 | m_ConstrainProportionsScale: 0
31 | m_Children: []
32 | m_Father: {fileID: 0}
33 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dev.vrlabs.custom-object-sync",
3 | "displayName": "Custom Object Sync",
4 | "version": "1.0.999",
5 | "license": "MIT",
6 | "unity": "2022.3",
7 | "description": "Customizable Object Sync",
8 | "author": {
9 | "name": "VRLabs",
10 | "email": "mail@vrlabs.dev",
11 | "url": "https://vrlabs.dev"
12 | },
13 | "siteUrl": "https://github.com/VRLabs/Custom-Object-Sync",
14 | "vpmDependencies": {
15 | "com.vrchat.avatars": "^3.7.0"
16 | },
17 | "legacyFolders": {
18 | },
19 | "unityPackageDestinationFolder": "Assets/VRLabs/Custom Object Sync",
20 | "vccRepoCategory": "systems",
21 | "media": {
22 | "previewImage": "https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/main/Media/Web/Preview.webp",
23 | "previewGif": "https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/main/Media/Web/PreviewGif.webp"
24 | },
25 | "unityPackageDestinationFolderMetas": {
26 | "Assets/VRLabs": "652a1ba5b00554143bc9a76307dbc4e8",
27 | "Assets/VRLabs/Custom Object Sync": "8dcb521ec80db344a9485178247c1567"
28 | },
29 | "questCompatibility": "full"
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 VRLabs LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/VRC-Asset-Release-And-Upload.yml:
--------------------------------------------------------------------------------
1 | name: VRC Asset Release and Listing Upload
2 | on:
3 | push:
4 | tags:
5 | - "*.*.*"
6 |
7 | env:
8 | ASSETS_PATH: .
9 | RELEASE_PATH: Packages
10 | ARTIFACT_DURATION: 30 # In days
11 | UPLOAD_ENDPOINT: https://api.vrlabs.dev/packages/add
12 | WORKFLOW_VERSION: 1.0.0
13 |
14 | jobs:
15 | build:
16 | runs-on: "ubuntu-latest"
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Check if package.json exists
22 | run: |
23 | if [ ! -f package.json ]; then
24 | echo "package.json not found"
25 | exit 1
26 | fi
27 |
28 | - name: Get package.json
29 | id: get_package_json
30 | run: |
31 | {
32 | echo 'package_json<<"""'
33 | echo $(cat package.json)
34 | echo '"""'
35 | } >> $GITHUB_OUTPUT
36 |
37 | - name: Get needed Data
38 | id: job_data
39 | run: |
40 | version=$(echo "${{ github.ref_name }}")
41 | version=$(echo $version | tr '[:upper:]' '[:lower:]')
42 | echo "version=$version" >> $GITHUB_OUTPUT
43 | major_version=$(echo $version | cut -d '.' -f 1)
44 | minor_version=$(echo $version | cut -d '.' -f 2)
45 | echo "major_version=$major_version" >> $GITHUB_OUTPUT
46 | echo "minor_version=$minor_version" >> $GITHUB_OUTPUT
47 | name="${{ fromJson(steps.get_package_json.outputs.package_json).name }}"
48 | display_name="${{ fromJson(steps.get_package_json.outputs.package_json).displayName }}"
49 | echo "package_name=$name" >> $GITHUB_OUTPUT
50 | echo "package_display_name=$display_name" >> $GITHUB_OUTPUT
51 |
52 | - name: Create Packages
53 | id: create_packages
54 | uses: VRLabs/VRCTools-Packaging-Action@v1
55 | with:
56 | path: '${{ env.ASSETS_PATH }}'
57 | outputPath: '${{ env.RELEASE_PATH }}'
58 | releaseUrl: 'https://github.com/${{ github.repository }}/releases/download/${{ steps.job_data.outputs.version }}/${{ steps.job_data.outputs.package_name }}-${{ steps.job_data.outputs.version }}.zip'
59 | unityReleaseUrl: 'https://github.com/${{ github.repository }}/releases/download/${{ steps.job_data.outputs.version }}/${{ steps.job_data.outputs.package_name }}-${{ steps.job_data.outputs.version }}.unitypackage'
60 | releaseVersion: '${{ steps.job_data.outputs.version }}'
61 |
62 | - name: Create Release
63 | uses: softprops/action-gh-release@v1
64 | if: startsWith(github.ref, 'refs/tags/')
65 | with:
66 | name: "${{ steps.job_data.outputs.package_display_name }} ${{ steps.job_data.outputs.version }}"
67 | files: |
68 | ${{ steps.create_packages.outputs.unityPackagePath }}
69 | ${{ steps.create_packages.outputs.vccPackagePath }}
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | SOURCE_TAG: ${{ steps.job_data.outputs.version }}
73 |
74 | - name: Add server-json to Artifacts
75 | uses: actions/upload-artifact@v3
76 | with:
77 | name: server-json
78 | path: ${{ steps.create_packages.outputs.serverPackageJsonPath }}
79 | retention-days: ${{ env.ARTIFACT_DURATION }}
80 |
81 | - name: Send package info to a server
82 | run: |
83 | curl -X POST -H "Content-Type: application/json" -H "Vrl-Api-Key: ${{ secrets.LISTINGS_API_KEY }}" --data @${{ steps.create_packages.outputs.serverPackageJsonPath }} ${{ env.UPLOAD_ENDPOINT }} || exit 0
84 | shell: bash
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Custom Object Sync
4 |
5 | [](https://github.com/VRLabs/Custom-Object-Sync/releases/latest)
6 | [](https://github.com/VRLabs/Custom-Object-Sync/blob/main/LICENSE)
7 | [](https://img.shields.io/badge/Quest-Compatible-green?logo=Meta)
8 | [](https://unity.com/releases/editor/whats-new/2022.3.22)
9 | [](https://vrchat.com/home/download)
10 |
11 | [](https://discord.vrlabs.dev/)
12 | [](https://patreon.vrlabs.dev/)
13 |
14 | Sync objects across the network with custom range, precision, and parameter usage.
15 |
16 | 
17 |
18 | ### ⬇️ [Download Latest Version](https://github.com/VRLabs/Custom-Object-Sync/releases/latest)
19 |
20 | ### 📦 [Add to VRChat Creator Companion](https://vrlabs.dev/packages?package=dev.vrlabs.custom-object-sync)
21 |
22 |
23 |
24 | ---
25 |
26 | ## How it works
27 |
28 | * Contacts and Physbones read the location and rotation of your object(s).
29 | * Parameter Drivers convert this location and rotation into boolean values.
30 | * Those boolean values are synced over the network in multiple steps. Choosing more steps means we can use fewer parameters.
31 | * Once the values arrive at the remote side, they get converted back into floats.
32 | * These floats get used to set the object back in its place on the remote side.
33 | * This can be useful when:
34 | * You want to late sync a world drop.
35 | * You want to fly an object around and have it sync reliably.
36 | * You have an object that moves based on some local-only/fps dependent mechanism.
37 |
38 | ## Install guide
39 |
40 | https://github.com/VRLabs/Custom-Object-Sync/assets/76777936/4d12a815-bb1f-4c03-b0fc-83f9ea98638a
41 |
42 | * Click `VRLabs -> Custom Object Sync` at the top of the screen.
43 | * Drag the object you want to sync into the `Objects to Sync` field.
44 | * Adjust the values until you're happy with them:
45 | * Add Local Debug View: Adds a toggle to view the object's remote position and rotation.
46 | * Quick Sync:
47 | * Position Precision: The position precision to be synced. Since this sync mode syncs floats, Position Precision also affects Range, and Rotation Precision is locked at 1 float per axis.
48 | * Non Quick Sync:
49 | * Position Precision: Precision of the synced object's Position.
50 | * Rotation Precision: Precision of the synced object's Rotation.
51 | * Radius: Radius of the sync. For more information, see `Sync Type`
52 | * Bits per Sync: Amount of bits per sync step. Lower bits means longer before the full object is synced, but also less parameter usage.
53 | * Enable Rotation Sync: Whether or not rotation should be Synced.
54 | * Sync Type: Whether or not the sync should be based on avatar root or on world origin.
55 | * Avatar Centered: The sync is based off of a point which is dropped when-ever you start the sync. This means you can use way lower range, but also it won't late sync.
56 | * This is forced for quick sync.
57 | * World Centered: The sync is based off of world origin. This means you'll need a big range to support big worlds, but it will late sync.
58 | * Add Damping Constraint to Object: Whether or not the remote object should be damped. This means the object moves to its new position smoothly whenever it receives a new position.
59 | * Damping value: How the damping behaves: 0 = don't move at all. 1 = move to the new position very fast.
60 | * Press Generate Custom Sync
61 |
62 | > [!NOTE]
63 | > When building for Quest, you will have to remove unsupported components and shaders
64 |
65 | ## How to use
66 |
67 | * Enable the `CustomObjectSync/Enabled` bool to start the sync.
68 | * Now the location of the Target objects will be synced over the network.
69 |
70 | ## Performance stats
71 |
72 | Rotation Sync:
73 |
74 | ```c++
75 | Constraints: 11-13 + 3 per object
76 | Contact Receivers: 6
77 | Contact Senders: 3
78 | FX Animator Layers: 2 + 2 per object
79 | Phys Bones: 6
80 | Phys Bone Colliders: 3
81 | Rigidbodies: 1
82 | Joints: 1
83 | Expression Parameters: 1-256
84 | ```
85 |
86 | No Rotation Sync:
87 |
88 | ```c++
89 | Constraints: 11-13 + 3 per object
90 | Contact Receivers: 6
91 | Contact Senders: 3
92 | FX Animator Layers: 2 + 1 per object
93 | Rigidbodies: 1
94 | Joints: 1
95 | Expression Parameters: 1-256
96 | ```
97 |
98 | ## Hierarchy layout
99 |
100 | Rotation Sync:
101 |
102 | ```html
103 | Custom Object Sync
104 | |-Target
105 | |-Measure
106 | | |-Position
107 | | | |-SenderX
108 | | | |-SenderY
109 | | | |-SenderZ
110 | | | |-Receiver X+
111 | | | |-Receiver X-
112 | | | |-Receiver Y+
113 | | | |-Receiver Y-
114 | | | |-Receiver Z+
115 | | | |-Receiver Z-
116 | | |-Rotation
117 | | | |-Measure Bones
118 | | | | |-Measure X Magnitude
119 | | | | |-Measure X Sign
120 | | | | |-Measure Y Magnitude
121 | | | | |-Measure Y Sign
122 | | | | |-Measure Z Magnitude
123 | | | | |-Measure Z Sign
124 | | | |-Measure Planes
125 | | | | |-X Angle Plane
126 | | | | |-Y Angle Plane
127 | | | | |-Z Angle Plane
128 | |-Set
129 | | |-Result
130 | ```
131 |
132 | No Rotation Sync:
133 |
134 | ```html
135 | Custom Object Sync
136 | |-Target
137 | |-Measure
138 | | |-Position
139 | | | |-SenderX
140 | | | |-SenderY
141 | | | |-SenderZ
142 | | | |-Receiver X+
143 | | | |-Receiver X-
144 | | | |-Receiver Y+
145 | | | |-Receiver Y-
146 | | | |-Receiver Z+
147 | | | |-Receiver Z-
148 | |-Set
149 | | |-Result
150 | ```
151 |
152 | ## Contributors
153 |
154 | * [jellejurre](https://github.com/jellejurre)
155 |
156 | ## License
157 |
158 | Custom Object Sync is available as-is under MIT. For more information see [LICENSE](https://github.com/VRLabs/Custom-Object-Sync/blob/main/LICENSE).
159 |
160 |
161 |
162 |
163 |
164 | [
](https://vrlabs.dev "VRLabs")
165 |
166 | [
](https://discord.vrlabs.dev/ "VRLabs")
167 |
168 | [
](https://patreon.vrlabs.dev/ "VRLabs")
169 |
170 | [
](https://twitter.com/vrlabsdev "VRLabs")
171 |
172 |
173 |
--------------------------------------------------------------------------------
/Editor/CustomObjectSyncCreatorWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Text;
4 | using UnityEditor;
5 | using UnityEditorInternal;
6 | using UnityEngine;
7 | using UnityEngine.Animations;
8 | using UnityEngine.Assertions.Must;
9 | using VRC.SDK3.Avatars.Components;
10 | using VRC.SDK3.Avatars.ScriptableObjects;
11 | using static UnityEditor.EditorGUILayout;
12 | using AnimatorController = UnityEditor.Animations.AnimatorController;
13 |
14 | namespace VRLabs.CustomObjectSyncCreator
15 | {
16 | public class CustomObjectSyncCreatorWindow : EditorWindow
17 | {
18 | private CustomObjectSyncCreator creator;
19 |
20 | [MenuItem("VRLabs/Custom Object Sync")]
21 | public static void OpenWindow()
22 | {
23 | EditorWindow w = GetWindow();
24 | w.titleContent = new GUIContent("Custom Object Sync");
25 | }
26 |
27 | Vector2 scrollPosition = Vector2.zero;
28 |
29 | private void OnGUI()
30 | {
31 | if (creator == null)
32 | {
33 | creator = CustomObjectSyncCreator.instance;
34 | }
35 |
36 | if (creator.resourcePrefab == null)
37 | {
38 | creator.resourcePrefab = AssetDatabase.LoadAssetAtPath("Assets/VRLabs/CustomObjectSync/Resources/Custom Object Sync.prefab");
39 | }
40 | if (creator.resourcePrefab == null)
41 | {
42 | creator.resourcePrefab = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath("d51eb264fa89a5b4d9b95f344f169766") ?? "");
43 | }
44 |
45 |
46 | if (creator.resourcePrefab == null)
47 | {
48 | Debug.LogError("Prefab Asset not found. Please reimport the Custom Object Sync package.");
49 | return;
50 | }
51 |
52 | GUILayout.Space(2);
53 |
54 | using (new VerticalScope(GUI.skin.box))
55 | {
56 | using (var scrollViewScope = new GUILayout.ScrollViewScope(scrollPosition))
57 | {
58 | scrollPosition = scrollViewScope.scrollPosition;
59 | if (creator.useMultipleObjects)
60 | {
61 | SerializedObject so = new SerializedObject(creator);
62 | SerializedProperty prop = so.FindProperty("syncObjects");
63 | ReorderableList list = new ReorderableList(so, prop, true, true, true, true);
64 | list.drawHeaderCallback = rect => { EditorGUI.LabelField(rect, "Objects To Sync"); };
65 | list.drawElementCallback = (rect, index, active, focused) =>
66 | {
67 | SerializedProperty element = prop.GetArrayElementAtIndex(index);
68 | EditorGUI.ObjectField(rect, element, typeof(GameObject));
69 | };
70 | list.DoLayoutList();
71 | so.ApplyModifiedProperties();
72 | }
73 | else
74 | {
75 | creator.syncObject = (GameObject)ObjectField("Object To Sync", creator.syncObject,
76 | typeof(GameObject), true);
77 | }
78 |
79 | GUILayout.Space(2);
80 | using (new GUILayout.HorizontalScope())
81 | {
82 | creator.useMultipleObjects =
83 | GUILayout.Toggle(creator.useMultipleObjects, new GUIContent("Sync Multiple Objects", "Sync multiple objects at once. This will increase sync times."));
84 | creator.quickSync = GUILayout.Toggle(creator.quickSync,
85 | new GUIContent("Quick Sync",
86 | "This will lower customizability but increase sync times by using 1 float per variable."));
87 | }
88 |
89 |
90 | if (!creator.ObjectPredicate(x => x != null))
91 | {
92 | GUILayout.Label("Please select an object to sync.");
93 | return;
94 | }
95 |
96 | if (creator.useMultipleObjects && creator.syncObjects.Count(x => x != null) !=
97 | creator.syncObjects.Where(x => x != null).Distinct().Count())
98 | {
99 | GUILayout.Label("Please select distinct objects to sync.");
100 | return;
101 | }
102 |
103 | VRCAvatarDescriptor descriptor = creator.syncObjects.FirstOrDefault(x => x != null)
104 | ?.GetComponentInParent();
105 |
106 | if (!descriptor)
107 | {
108 | GUILayout.Label(
109 | "Object is not a child of an Avatar. Please select an object that is a child of an Avatar.");
110 | return;
111 | }
112 |
113 | if (descriptor != null &&
114 | descriptor.baseAnimationLayers
115 | .FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController !=
116 | null &&
117 | ((AnimatorController)descriptor.baseAnimationLayers
118 | .FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController)
119 | .layers
120 | .Any(x => x.name.Contains("CustomObjectSync")))
121 | {
122 | using (new HorizontalScope(GUI.skin.box))
123 | {
124 | GUILayout.FlexibleSpace();
125 | using (new VerticalScope())
126 | {
127 | GUILayout.Label(
128 | $"Custom Object Sync found on avatar. Multiple custom object syncs not supported.",
129 | new[] { GUILayout.ExpandWidth(true) });
130 | using (new HorizontalScope())
131 | {
132 | GUILayout.FlexibleSpace();
133 | GUILayout.Label($"Please remove previous custom object sync before continuing.",
134 | new[] { GUILayout.ExpandWidth(true) });
135 | GUILayout.FlexibleSpace();
136 | }
137 |
138 | if (GUILayout.Button("Remove Custom Sync"))
139 | {
140 | creator.Remove();
141 | }
142 | }
143 |
144 | GUILayout.FlexibleSpace();
145 | }
146 |
147 | return;
148 | }
149 |
150 | if (creator.ObjectPredicate(x => Equals(descriptor.gameObject, x)))
151 | {
152 | GUILayout.Label(
153 | "Object has a VRC Avatar Descriptor. The object you select should be the object you want to sync, not the avatar.");
154 | return;
155 | }
156 |
157 | GUILayout.Space(2);
158 |
159 | creator.menuLocation = TextField(new GUIContent("Menu Location", "The menu path to the menu in which the enable toggle will be placed. e.g. /Props/Ball"), creator.menuLocation);
160 |
161 | VRCExpressionsMenu menu = creator.GetMenuFromLocation(descriptor, creator.menuLocation);
162 |
163 | if (creator.menuLocation != "" && creator.menuLocation != "/")
164 | {
165 | if (menu == null || menu.controls == null)
166 | {
167 | GUILayout.Label("Menu Location not found. Please select a valid menu location (e.g. /Props/Ball)");
168 | return;
169 | }
170 | }
171 |
172 | if (menu != null && (menu.controls.Count == 8 || (creator.addLocalDebugView && menu.controls.Count >= 7)))
173 | {
174 | GUILayout.Label("Menu Location is too full. Please select a menu location that has more space left or make some space in your selected menu location");
175 | return;
176 | }
177 |
178 |
179 |
180 | GUILayout.Space(2);
181 |
182 | using (new HorizontalScope(GUI.skin.box))
183 | {
184 | creator.writeDefaults = GUILayout.Toggle(creator.writeDefaults, new GUIContent("Write Defaults", "Whether to use Write Defaults on or Write Defaults off for the generated states."));
185 | creator.addLocalDebugView = GUILayout.Toggle(creator.addLocalDebugView,
186 | new GUIContent("Add Local Debug View",
187 | "Adds a local debug view to the object. This will show the remote position and rotation of the object, locally."));
188 | }
189 |
190 | GUILayout.Space(2);
191 |
192 | if (creator.quickSync)
193 | {
194 | DisplayQuickSyncGUI();
195 | }
196 | else
197 | {
198 | DisplayBitwiseGUI();
199 | }
200 |
201 | GUILayout.Space(2);
202 |
203 | if (GUILayout.Button("Generate Custom Sync"))
204 | {
205 | EditorApplication.delayCall += creator.Generate;
206 | }
207 | }
208 | }
209 | }
210 |
211 | private void DisplayQuickSyncGUI()
212 | {
213 | creator.maxRadius = 8 - creator.positionPrecision;
214 | creator.rotationPrecision = 8;
215 |
216 | string positionString = $"Position Precision: {FloatToStringConverter.Convert((float)Math.Pow(0.5, creator.positionPrecision) * 100)}cm";
217 | creator.positionPrecision = DisplayInt(positionString, creator.positionPrecision, 1, 8);
218 |
219 | GUILayout.Space(2);
220 | using (new HorizontalScope(GUI.skin.box))
221 | {
222 | GUILayout.Label($"Radius: {Math.Pow(2, 8 - creator.positionPrecision)}m");
223 | }
224 |
225 |
226 | GUILayout.Space(2);
227 |
228 | int objectCount = creator.useMultipleObjects ? creator.syncObjects.Count(x => x != null): 1;
229 |
230 | using (new HorizontalScope(GUI.skin.box))
231 | {
232 | creator.rotationEnabled = GUILayout.Toggle(creator.rotationEnabled, "Enable Rotation Sync");
233 | }
234 |
235 | GUILayout.Space(2);
236 |
237 | using (new HorizontalScope(GUI.skin.box))
238 | {
239 | creator.addDampeningConstraint = GUILayout.Toggle(creator.addDampeningConstraint, "Add Damping Constraint to Object");
240 | if (creator.addDampeningConstraint)
241 | {
242 | GUILayout.Space(5);
243 | creator.dampingConstraintValue = EditorGUILayout.Slider("Damping Value", creator.dampingConstraintValue, 0, 1);
244 | }
245 | }
246 |
247 | GUILayout.Space(2);
248 |
249 | int objectParameterCount = Mathf.CeilToInt(Mathf.Log(objectCount, 2));
250 | GUI.contentColor = Color.white;
251 |
252 | using (new HorizontalScope(GUI.skin.box))
253 | {
254 | GUILayout.FlexibleSpace();
255 | int bitUsage = creator.GetMaxBitCount();
256 | GUILayout.Label($"Parameter Usage: {objectParameterCount + bitUsage + 1}, Time per Sync: {(objectCount * (1/5f)), 4:F3}s", new [] { GUILayout.ExpandWidth(true) });
257 | GUILayout.FlexibleSpace();
258 | }
259 | }
260 | private void DisplayBitwiseGUI()
261 | {
262 | string positionString = $"Position Precision: {FloatToStringConverter.Convert((float)Math.Pow(0.5, creator.positionPrecision - 1) * 100)}cm";
263 | creator.positionPrecision = DisplayInt(positionString, creator.positionPrecision, 2, 16);
264 |
265 | GUILayout.Space(2);
266 |
267 | if (creator.rotationEnabled)
268 | {
269 | string rotationString = $"Rotation Precision: {(360.0 / Math.Pow(2, creator.rotationPrecision)).ToString("G3")}\u00b0";
270 | creator.rotationPrecision = DisplayInt(rotationString, creator.rotationPrecision, 0, 16);
271 |
272 | GUILayout.Space(2);
273 | }
274 |
275 | creator.maxRadius = DisplayInt($"Radius: {(Math.Pow(2, creator.maxRadius)).ToString("G5")}m", creator.maxRadius, 3, 13);
276 |
277 | if (creator.maxRadius > 11)
278 | {
279 | Color oldColor = GUI.color;
280 | GUI.color = Color.yellow;
281 | GUILayout.Label("Warning: Radius is very high.\nMost worlds don't need this high radius, and it will cause the position to jitter when walking around.");
282 | GUI.color = oldColor;
283 | }
284 |
285 | GUILayout.Space(2);
286 |
287 | var maxbitCount = creator.GetMaxBitCount();
288 |
289 |
290 | int syncSteps = creator.GetStepCount();
291 | int objectCount = creator.useMultipleObjects ? creator.syncObjects.Count(x => x != null): 1;
292 |
293 | while (syncSteps == creator.GetStepCount(creator.bitCount - 1) && creator.bitCount != 1)
294 | {
295 | creator.bitCount--;
296 | }
297 | using (new HorizontalScope(GUI.skin.box))
298 | {
299 | GUILayout.Label($"Bits per Sync: {creator.bitCount}", new GUILayoutOption[]{ GUILayout.MaxWidth(360)});
300 | if (GUILayout.Button(EditorGUIUtility.IconContent("d_Toolbar Minus")) && creator.bitCount != 1)
301 | {
302 | creator.bitCount--;
303 | while (creator.GetStepCount() == creator.GetStepCount(creator.bitCount-1) && creator.bitCount != 1)
304 | {
305 | creator.bitCount--;
306 | }
307 | }
308 |
309 | if (GUILayout.Button(EditorGUIUtility.IconContent("d_Toolbar Plus")) && creator.bitCount != maxbitCount)
310 | {
311 | while (syncSteps == creator.GetStepCount(creator.bitCount + 1) && creator.bitCount != maxbitCount)
312 | {
313 | creator.bitCount++;
314 | }
315 |
316 | if (creator.bitCount != maxbitCount + 1)
317 | {
318 | creator.bitCount++;
319 | }
320 | }
321 | }
322 |
323 | GUILayout.Space(2);
324 |
325 | using (new HorizontalScope(GUI.skin.box))
326 | {
327 | creator.rotationEnabled = GUILayout.Toggle(creator.rotationEnabled, "Enable Rotation Sync");
328 | creator.centeredOnAvatar = EditorGUILayout.Popup(new GUIContent("Sync Type",
329 | "Avatar Centered drops the sync point at the avatar's base when enabling sync. This means radius can be way lower, but it is not late join synced.\n" +
330 | "World Centered syncs from world origin, which means larger radius is required, but it is late join synced."), (creator.centeredOnAvatar ? 1 : 0),
331 | new GUIContent[]
332 | {
333 | new GUIContent("World Centered", "Syncs from world origin, which means larger radius is required, but it is late join synced."),
334 | new GUIContent("Avatar Centered", "Drops the sync point at the avatar's base when enabling sync. This means radius can be way lower, but it is not late join synced.")
335 | } ) == 1;
336 | }
337 |
338 | GUILayout.Space(2);
339 |
340 | using (new HorizontalScope(GUI.skin.box))
341 | {
342 | creator.addDampeningConstraint = GUILayout.Toggle(creator.addDampeningConstraint, new GUIContent("Add Damping Constraint to Object", "Adds a damping constraint on the remote side, this makes the object slowly move to the new synced point instead of snapping to it."));
343 | if (creator.addDampeningConstraint)
344 | {
345 | GUILayout.Space(5);
346 | creator.dampingConstraintValue = EditorGUILayout.Slider("Damping Value", creator.dampingConstraintValue, 0, 1);
347 | }
348 | }
349 |
350 | GUILayout.Space(2);
351 |
352 | int parameterCount = Mathf.CeilToInt(Mathf.Log(syncSteps + 1, 2));
353 | int objectParameterCount = Mathf.CeilToInt(Mathf.Log(objectCount, 2));
354 | GUI.contentColor = Color.white;
355 |
356 | using (new HorizontalScope(GUI.skin.box))
357 | {
358 | GUILayout.FlexibleSpace();
359 | float conversionTime = (Math.Max(creator.rotationPrecision, creator.maxRadius + creator.positionPrecision)) * 1.5f / 60f;
360 | float timeBetweenSyncs = objectCount * syncSteps * (1/5f);
361 | GUILayout.Label($"Sync Steps: {syncSteps}, Parameter Usage: {objectParameterCount + parameterCount + creator.bitCount + 1}, Sync Rate: {(timeBetweenSyncs), 4:F3}s, Sync Delay: {(timeBetweenSyncs + (2 * conversionTime)), 4:F3}s", new [] { GUILayout.ExpandWidth(true) });
362 | GUILayout.FlexibleSpace();
363 | }
364 | }
365 |
366 | public int DisplayInt(string s, int value, int lowerBound, int upperbound)
367 | {
368 | value = Mathf.Clamp(value, lowerBound, upperbound);
369 | using (new HorizontalScope(GUI.skin.box))
370 | {
371 | GUILayout.Label(s, new GUILayoutOption[]{ GUILayout.MaxWidth(360)});
372 | if (GUILayout.Button(EditorGUIUtility.IconContent("d_Toolbar Minus")) && value != lowerBound)
373 | {
374 | value--;
375 | }
376 |
377 | if (GUILayout.Button(EditorGUIUtility.IconContent("d_Toolbar Plus")) && value != upperbound)
378 | {
379 | value++;
380 | }
381 | return value;
382 | }
383 | }
384 | }
385 |
386 |
387 | class FloatToStringConverter
388 | {
389 | public static string Convert(float value)
390 | {
391 | // Handle special cases
392 | if (float.IsInfinity(value) || float.IsNaN(value))
393 | {
394 | return value.ToString();
395 | }
396 |
397 | // If value is a whole number, use standard formatting with no decimal places
398 | if (value % 1 == 0)
399 | {
400 | return value.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
401 | }
402 |
403 | StringBuilder sb = new StringBuilder();
404 | // Consider negative sign
405 | if (value < 0)
406 | {
407 | sb.Append('-');
408 | value = Math.Abs(value);
409 | }
410 |
411 | // Append the whole number part
412 | long wholePart = (long)value;
413 | sb.Append(wholePart);
414 |
415 |
416 | // Work on the fractional part
417 | float fractionalPart = value - wholePart;
418 | if (fractionalPart != 0)
419 | {
420 | sb.Append(".");
421 | }
422 | int significantDigits = 0;
423 | bool nonZeroDigitEncountered = false;
424 |
425 | // Handle up to three significant digits in the fractional part
426 | while (significantDigits < 3)
427 | {
428 | fractionalPart *= 10; // Shift the decimal point by one position to the right
429 | int digit = (int)fractionalPart;
430 | sb.Append(digit); // Append the digit
431 | if (digit != 0 || nonZeroDigitEncountered)
432 | {
433 | nonZeroDigitEncountered = true;
434 | significantDigits++;
435 | }
436 | fractionalPart -= digit; // Remove the digit we just processed
437 | if (fractionalPart <= 0)
438 | {
439 | break; // No more non-zero digits in the fractional part
440 | }
441 | }
442 |
443 | return sb.ToString();
444 | }
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/Editor/ControllerGenerationMethods.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using UnityEditor;
6 | using UnityEditor.Animations;
7 | using UnityEngine;
8 | using VRC.SDK3.Avatars.Components;
9 | using VRC.SDK3.Avatars.ScriptableObjects;
10 | using VRC.SDKBase;
11 | using static VRC.SDKBase.VRC_AnimatorTrackingControl;
12 | using static VRC.SDKBase.VRC_AvatarParameterDriver;
13 | using Object = UnityEngine.Object;
14 |
15 | namespace VRLabs.CustomObjectSyncCreator
16 | {
17 | public class ControllerGenerationMethods
18 | {
19 |
20 | public static bool defaultWriteDefaults = false;
21 | public static AnimationClip GenerateClip(string name, Bounds localBounds = default, float frameRate = 60f, WrapMode wrapMode = WrapMode.Default, AnimationEvent[] events = null)
22 | {
23 | return new AnimationClip()
24 | {
25 | name = name,
26 | wrapMode = wrapMode,
27 | localBounds = localBounds,
28 | frameRate = frameRate,
29 | legacy = false
30 | };
31 | }
32 |
33 | public static AnimationCurve GenerateCurve(Keyframe[] keys, WrapMode preWrapMode = WrapMode.ClampForever, WrapMode postWrapMode = WrapMode.ClampForever)
34 | {
35 | return new AnimationCurve()
36 | {
37 | keys = keys,
38 | preWrapMode = preWrapMode,
39 | postWrapMode = postWrapMode
40 | };
41 | }
42 |
43 | public static ObjectReferenceKeyframe GenerateObjectReferenceKeyFrame(float time, Object obj)
44 | {
45 | return new ObjectReferenceKeyframe()
46 | {
47 | time = time,
48 | value = obj
49 | };
50 | }
51 |
52 | public static Keyframe GenerateKeyFrame(float time = 0, float value = 0, float inTangent = 0, float outTangent = 0, float inWeight = 0, float outWeight = 0)
53 | {
54 | Keyframe frame = new Keyframe()
55 | {
56 | time = time,
57 | value = value,
58 | inTangent = inTangent,
59 | outTangent = outTangent,
60 | inWeight = inWeight,
61 | outWeight = outWeight,
62 | };
63 | frame.weightedMode = (inWeight != 0 || outWeight != 0) ? WeightedMode.Both : WeightedMode.None;
64 | return frame;
65 | }
66 |
67 | // Redundant? Maybe. But I'm gonna use it anyways hehe
68 | public static void AddCurve(AnimationClip clip, string relativePath, Type type, string propertyName, AnimationCurve curve)
69 | {
70 | clip.SetCurve(relativePath, type, propertyName, curve);
71 | }
72 |
73 | public static void AddObjectCurve(AnimationClip clip, string relativePath, Type type, string propertyName, ObjectReferenceKeyframe[] curve)
74 | {
75 | AnimationUtility.SetObjectReferenceCurve(clip, new EditorCurveBinding()
76 | {
77 | path = relativePath,
78 | propertyName = propertyName,
79 | type = type
80 | }, curve);
81 | }
82 |
83 | public static BlendTree GenerateBlendTree(string name, BlendTreeType blendType, string blendParameter = "", string blendParameterY = "", float minThreshold = 0, float maxThreshold = 1, bool useAutomaticThresholds = true)
84 | {
85 | return new BlendTree()
86 | {
87 | name = name,
88 | blendType = blendType,
89 | blendParameter = blendParameter,
90 | blendParameterY = blendParameterY,
91 | maxThreshold = maxThreshold,
92 | minThreshold = minThreshold,
93 | useAutomaticThresholds = useAutomaticThresholds
94 | };
95 | }
96 |
97 | public static ChildMotion GenerateChildMotion(Motion motion = null, Vector2 position = default, float threshold = 0.0f, float timeScale = 1.0f, string directBlendParameter = "", float cycleOffset = 0.0f, bool mirror = false)
98 | {
99 | return new ChildMotion()
100 | {
101 | motion = motion,
102 | position = position,
103 | threshold = threshold,
104 | timeScale = timeScale,
105 | directBlendParameter = directBlendParameter,
106 | cycleOffset = cycleOffset,
107 | mirror = mirror
108 | };
109 | }
110 |
111 |
112 | public static AnimatorStateMachine GenerateStateMachine(string name,
113 | Vector3 anyStatePosition, Vector3 entryPosition, Vector3 exitPosition,
114 | ChildAnimatorState[] states = null, AnimatorState defaultState = null,
115 | StateMachineBehaviour[] behaviours = null,
116 | AnimatorStateTransition[] anyStateTransitions = null,
117 | AnimatorTransition[] entryTransitions = null,
118 | Vector3 parentStateMachinePosition = default, ChildAnimatorStateMachine[] stateMachines = null)
119 | {
120 | return new AnimatorStateMachine()
121 | {
122 | name = name,
123 | anyStatePosition = anyStatePosition,
124 | entryPosition = entryPosition,
125 | exitPosition = exitPosition,
126 | states = states ?? Array.Empty(),
127 | defaultState = defaultState,
128 | behaviours = behaviours ?? Array.Empty(),
129 | anyStateTransitions = anyStateTransitions ?? Array.Empty(),
130 | entryTransitions = entryTransitions ?? Array.Empty(),
131 | parentStateMachinePosition = parentStateMachinePosition,
132 | stateMachines = stateMachines
133 | };
134 | }
135 |
136 | public static ChildAnimatorStateMachine GenerateChildStateMachine(Vector3 position, AnimatorStateMachine stateMachine)
137 | {
138 | return new ChildAnimatorStateMachine()
139 | {
140 | position = position,
141 | stateMachine = stateMachine
142 | };
143 | }
144 |
145 | public static AnimatorState GenerateState(string name, bool writeDefaultValues = false, string tag = "",
146 | Motion motion = null, AnimatorStateTransition[] transitions = null,
147 | float cycleOffset = 0.0f, string cycleOffsetParameter = "", bool cycleOffsetParameterActive = false,
148 | bool mirror = false, string mirrorParameter = "", bool mirrorParameterActive = false,
149 | string timeParameter = "", bool timeParameterActive = false,
150 | float speed = 1.0f, string speedParameter = "", bool speedParameterActive = false
151 | )
152 | {
153 | return new AnimatorState()
154 | {
155 | name = name,
156 | writeDefaultValues = writeDefaultValues || defaultWriteDefaults,
157 | tag = tag,
158 | motion = motion,
159 | transitions = transitions ?? Array.Empty(),
160 | cycleOffset = cycleOffset,
161 | cycleOffsetParameter = cycleOffsetParameter,
162 | cycleOffsetParameterActive = cycleOffsetParameterActive,
163 | mirror = mirror,
164 | mirrorParameter = mirrorParameter,
165 | mirrorParameterActive = mirrorParameterActive,
166 | timeParameter = timeParameter,
167 | timeParameterActive = timeParameterActive,
168 | speed = speed,
169 | speedParameter = speedParameter,
170 | speedParameterActive = speedParameterActive
171 | };
172 | }
173 |
174 | public static ChildAnimatorState GenerateChildState(Vector3 position, AnimatorState state)
175 | {
176 | return new ChildAnimatorState()
177 | {
178 | position = position,
179 | state = state
180 | };
181 | }
182 |
183 | public static AnimatorStateTransition GenerateTransition(string name, bool canTransitionToSelf = false, AnimatorCondition[] conditions = null,
184 | AnimatorState destinationState = null, AnimatorStateMachine destinationStateMachine = null,
185 | float duration = 0.0f, bool hasFixedDuration = false,
186 | float exitTime = 0.0f, bool hasExitTime = false,
187 | bool solo = false, bool mute = false, bool isExit = false,
188 | float offset = 0.0f, bool orderedInterruption = true, TransitionInterruptionSource interruptionSource = TransitionInterruptionSource.None)
189 | {
190 | return new AnimatorStateTransition()
191 | {
192 | name = name,
193 | canTransitionToSelf = canTransitionToSelf,
194 | conditions = conditions ?? Array.Empty(),
195 | destinationState = destinationState,
196 | destinationStateMachine = destinationStateMachine,
197 | duration = duration,
198 | hasFixedDuration = hasFixedDuration,
199 | exitTime = exitTime,
200 | hasExitTime = hasExitTime,
201 | solo = solo,
202 | mute = mute,
203 | isExit = isExit,
204 | offset = offset,
205 | orderedInterruption = orderedInterruption,
206 | interruptionSource = interruptionSource,
207 | };
208 | }
209 |
210 | public static AnimatorTransition GenerateStateMachineTransition(string name, AnimatorCondition[] conditions = null,
211 | AnimatorState destinationState = null, AnimatorStateMachine destinationStateMachine = null,
212 | bool solo = false, bool mute = false, bool isExit = false)
213 | {
214 | return new AnimatorTransition()
215 | {
216 | name = name,
217 | conditions = conditions ?? Array.Empty(),
218 | destinationState = destinationState,
219 | destinationStateMachine = destinationStateMachine,
220 | solo = solo,
221 | mute = mute,
222 | isExit = isExit,
223 | };
224 | }
225 |
226 | public static AnimatorCondition GenerateCondition(AnimatorConditionMode mode, string parameter, float threshold)
227 | {
228 | return new AnimatorCondition()
229 | {
230 | mode = mode,
231 | parameter = parameter,
232 | threshold = threshold
233 | };
234 | }
235 |
236 | public static AnimatorControllerLayer GenerateLayer(string name, AnimatorStateMachine stateMachine, AvatarMask mask = null, AnimatorLayerBlendingMode blendingMode = AnimatorLayerBlendingMode.Override,
237 | float defaultWeight = 1.0f, bool syncedLayerAffectsTiming = false, int syncedLayerIndex = -1)
238 | {
239 | return new AnimatorControllerLayer()
240 | {
241 | name = name,
242 | stateMachine = stateMachine,
243 | avatarMask = mask,
244 | blendingMode = blendingMode,
245 | defaultWeight = defaultWeight,
246 | syncedLayerAffectsTiming = syncedLayerAffectsTiming,
247 | syncedLayerIndex = syncedLayerIndex
248 | };
249 | }
250 |
251 | public static AnimatorControllerParameter GenerateIntParameter(string name, int defaultInt = 0)
252 | {
253 | return new AnimatorControllerParameter()
254 | {
255 | name = name,
256 | defaultInt = defaultInt,
257 | type = AnimatorControllerParameterType.Int,
258 | };
259 | }
260 |
261 | public static AnimatorControllerParameter GenerateFloatParameter(string name, float defaultFloat = 0.0f)
262 | {
263 | return new AnimatorControllerParameter()
264 | {
265 | name = name,
266 | defaultFloat = defaultFloat,
267 | type = AnimatorControllerParameterType.Float,
268 | };
269 | }
270 |
271 | public static AnimatorControllerParameter GenerateBoolParameter(string name, bool defaultBool = false)
272 | {
273 | return new AnimatorControllerParameter()
274 | {
275 | name = name,
276 | defaultBool = defaultBool,
277 | type = AnimatorControllerParameterType.Bool,
278 | };
279 | }
280 |
281 | public static AnimatorControllerParameter GenerateTriggerParameter(string name)
282 | {
283 | return new AnimatorControllerParameter()
284 | {
285 | name = name,
286 | type = AnimatorControllerParameterType.Trigger,
287 | };
288 | }
289 |
290 | public static AvatarMask GenerateMask(string name, string[] transforms, bool[] values, AvatarMaskBodyPart[] bodyParts)
291 | {
292 | AvatarMask mask = new AvatarMask()
293 | {
294 | name = name,
295 | transformCount = transforms.Length
296 | };
297 |
298 | for (var i = 0; i < transforms.Length; i++)
299 | {
300 | mask.SetTransformPath(i, transforms[i]);
301 | mask.SetTransformActive(i, values[i]);
302 | }
303 |
304 | foreach (var part in Enum.GetValues(typeof(AvatarMaskBodyPart)).Cast().Where(x => x != AvatarMaskBodyPart.LastBodyPart))
305 | {
306 | mask.SetHumanoidBodyPartActive(part, bodyParts.Contains(part));
307 | }
308 |
309 | return mask;
310 | }
311 |
312 |
313 | public static VRCAvatarParameterDriver GenerateParameterDriver(Parameter[] parameters, bool isEnabled = true, bool localOnly = false, string debugString = "")
314 | {
315 | VRCAvatarParameterDriver driver = ScriptableObject.CreateInstance();
316 | driver.parameters = parameters.ToList();
317 | driver.debugString = debugString;
318 | driver.isEnabled = isEnabled;
319 | driver.localOnly = localOnly;
320 | return driver;
321 | }
322 |
323 | public static Parameter GenerateParameter(ChangeType type, string source = "", string name = "", float value = 0f, float chance = 0f, bool convertRange = false, float destMax = 0, float destMin = 0,
324 | float sourceMin = 0, float sourceMax = 0, float valueMin = 0, float valueMax = 0)
325 | {
326 | return new Parameter()
327 | {
328 | type = type,
329 | source = source,
330 | name = name,
331 | value = value,
332 | chance = chance,
333 | convertRange = convertRange,
334 | destMax = destMax,
335 | destMin = destMin,
336 | sourceMin = sourceMin,
337 | sourceMax = sourceMax,
338 | valueMin = valueMin,
339 | valueMax = valueMax
340 | };
341 | }
342 |
343 | public static VRCAnimatorLayerControl GenerateAnimatorLayerControl(VRC_AnimatorLayerControl.BlendableLayer playable, int layer = 0, float blendDuration = 0, float goalWeight = 1, string debugString = "")
344 | {
345 | VRCAnimatorLayerControl control = ScriptableObject.CreateInstance();
346 | control.playable = playable;
347 | control.layer = layer;
348 | control.blendDuration = blendDuration;
349 | control.goalWeight = goalWeight;
350 | control.debugString = debugString;
351 | return control;
352 | }
353 |
354 | public static VRCAnimatorLocomotionControl GenerateLocomotionControl(bool disableLocomotion, string debugString = "")
355 | {
356 | VRCAnimatorLocomotionControl control = ScriptableObject.CreateInstance();
357 | control.disableLocomotion = disableLocomotion;
358 | control.debugString = debugString;
359 | return control;
360 | }
361 |
362 | public static VRCAnimatorTrackingControl GenerateTrackingControl(TrackingType trackingHead = TrackingType.NoChange,
363 | TrackingType trackingLeftHand = TrackingType.NoChange,
364 | TrackingType trackingRightHand = TrackingType.NoChange,
365 | TrackingType trackingHip = TrackingType.NoChange,
366 | TrackingType trackingLeftFoot = TrackingType.NoChange,
367 | TrackingType trackingRightFoot = TrackingType.NoChange,
368 | TrackingType trackingLeftFingers = TrackingType.NoChange,
369 | TrackingType trackingRightFingers = TrackingType.NoChange,
370 | TrackingType trackingEyes = TrackingType.NoChange,
371 | TrackingType trackingMouth = TrackingType.NoChange,
372 | string debugString = "")
373 | {
374 | VRCAnimatorTrackingControl control = ScriptableObject.CreateInstance();
375 | control.trackingHead = trackingHead;
376 | control.trackingLeftHand = trackingLeftHand;
377 | control.trackingRightHand = trackingRightHand;
378 | control.trackingHip = trackingHip;
379 | control.trackingLeftFoot = trackingLeftFoot;
380 | control.trackingRightFoot = trackingRightFoot;
381 | control.trackingLeftFingers = trackingLeftFingers;
382 | control.trackingRightFingers = trackingRightFingers;
383 | control.trackingEyes = trackingEyes;
384 | control.trackingMouth = trackingMouth;
385 | control.trackingEyes = trackingEyes;
386 | control.debugString = debugString;
387 | return control;
388 | }
389 |
390 | public static VRCPlayableLayerControl GeneratePlayableLayerControl(VRC_PlayableLayerControl.BlendableLayer layer, float blendDuration = 0, float goalWeight = 1, string debugString = "")
391 | {
392 | VRCPlayableLayerControl control = ScriptableObject.CreateInstance();
393 | control.layer = layer;
394 | control.blendDuration = blendDuration;
395 | control.goalWeight = goalWeight;
396 | control.debugString = debugString;
397 | return control;
398 | }
399 |
400 | public static VRCAnimatorTemporaryPoseSpace GenerateTemporaryPoseSpace(float delayTime = 0, bool fixedDelay = false, bool enterPoseSpace = true, string debugString = "")
401 | {
402 | VRCAnimatorTemporaryPoseSpace control = ScriptableObject.CreateInstance();
403 | control.delayTime = delayTime;
404 | control.fixedDelay = fixedDelay;
405 | control.enterPoseSpace = enterPoseSpace;
406 | control.debugString = debugString;
407 | return control;
408 | }
409 |
410 | public static AnimatorController GenerateController(string name, AnimatorControllerLayer[] layers, AnimatorControllerParameter[] parameters)
411 | {
412 | return new AnimatorController()
413 | {
414 | name = name,
415 | layers = layers,
416 | parameters = parameters
417 | };
418 | }
419 |
420 | public static void SerializeController(AnimatorController controller)
421 | {
422 | controller.layers.ToList().ForEach(x => SerializeStateMachine(controller, x.stateMachine));
423 | }
424 |
425 | public static void SerializeStateMachine(AnimatorController controller, AnimatorStateMachine stateMachine)
426 | {
427 | Add(controller, stateMachine);
428 | foreach (var childAnimatorState in stateMachine.states)
429 | {
430 | AnimatorState animatorState = childAnimatorState.state;
431 |
432 | Add(controller, animatorState);
433 | animatorState.transitions.ToList().ForEach(x => Add(controller, x));
434 | animatorState.behaviours.ToList().ForEach(x => Add(controller, x));
435 |
436 | if (animatorState.motion is BlendTree tree)
437 | {
438 | Queue trees = new Queue(new [] {tree});
439 | while (trees.Count>0)
440 | {
441 | tree = trees.Dequeue();
442 | AssetDatabase.RemoveObjectFromAsset(tree);
443 | Add(controller, tree);
444 | tree.children.Where(x => x.motion is BlendTree).ToList().ForEach(x => trees.Enqueue((BlendTree)x.motion));
445 | }
446 | }
447 | }
448 |
449 | stateMachine.entryTransitions.ToList().ForEach(x => Add(controller, x));
450 | stateMachine.anyStateTransitions.ToList().ForEach(x => Add(controller, x));
451 | stateMachine.stateMachines.ToList().ForEach(x => SerializeStateMachine(controller, x.stateMachine));
452 | }
453 |
454 | public static void Add(AnimatorController controller, Object o){
455 | o.hideFlags = HideFlags.HideInHierarchy;
456 | AssetDatabase.RemoveObjectFromAsset(o);
457 | AssetDatabase.AddObjectToAsset(o, controller);
458 | }
459 |
460 | public static VRCExpressionParameters.Parameter GenerateVRCParameter(string name, VRCExpressionParameters.ValueType valueType, float defaultValue = 0.0f, bool saved = false, bool networkSynced = true)
461 | {
462 | return new VRCExpressionParameters.Parameter()
463 | {
464 | name = name,
465 | valueType = valueType,
466 | defaultValue = defaultValue,
467 | saved = saved,
468 | networkSynced = networkSynced
469 | };
470 | }
471 | }
472 | }
473 | #endif
--------------------------------------------------------------------------------
/Editor/CustomObjectSyncCreator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using UnityEditor;
7 | using UnityEditor.Animations;
8 | using UnityEditor.VersionControl;
9 | using UnityEngine;
10 | using UnityEngine.Animations;
11 | using VRC.Dynamics;
12 | using VRC.SDK3.Avatars.Components;
13 | using VRC.SDK3.Avatars.ScriptableObjects;
14 | using VRC.SDK3.Dynamics.Constraint.Components;
15 | using VRC.SDK3.Dynamics.Contact.Components;
16 | using static VRC.SDKBase.VRC_AvatarParameterDriver;
17 | using static VRLabs.CustomObjectSyncCreator.ControllerGenerationMethods;
18 | using AnimationCurve = UnityEngine.AnimationCurve;
19 | using GameObject = UnityEngine.GameObject;
20 | using Object = UnityEngine.Object;
21 |
22 | namespace VRLabs.CustomObjectSyncCreator
23 | {
24 | public class CustomObjectSyncCreator : ScriptableSingleton
25 | {
26 | public GameObject resourcePrefab;
27 | public int bitCount = 16;
28 | public int maxRadius = 7;
29 | public int positionPrecision = 6;
30 | public int rotationPrecision = 8;
31 | public bool rotationEnabled = true;
32 | public bool centeredOnAvatar = false;
33 | public bool addDampeningConstraint = false;
34 | public bool useMultipleObjects = false;
35 | public GameObject[] syncObjects;
36 | public float dampingConstraintValue = 0.15f;
37 | public bool quickSync;
38 | public bool writeDefaults = true;
39 | public bool addLocalDebugView = false;
40 | public string menuLocation = "";
41 |
42 | public const string STANDARD_NEW_ANIMATION_FOLDER = "Assets/VRLabs/GeneratedAssets/CustomObjectSync/Animations/";
43 | public const string STANDARD_NEW_ANIMATOR_FOLDER = "Assets/VRLabs/GeneratedAssets/CustomObjectSync/Animators/";
44 | public const string STANDARD_NEW_PARAMASSET_FOLDER = "Assets/VRLabs/GeneratedAssets/CustomObjectSync/ExpressionParameters/";
45 | public const string STANDARD_NEW_MENUASSET_FOLDER = "Assets/VRLabs/GeneratedAssets/CustomObjectSync/ExpressionMenu/";
46 |
47 | public GameObject syncObject
48 | {
49 | get
50 | {
51 | if (syncObjects == null)
52 | {
53 | syncObjects = new GameObject[1];
54 | }
55 | Array.Resize(ref syncObjects, 1);
56 | return syncObjects[0];
57 | }
58 | set
59 | {
60 | if (syncObjects == null)
61 | {
62 | syncObjects = new GameObject[1];
63 | }
64 | Array.Resize(ref syncObjects, 1);
65 | syncObjects[0] = value;
66 | }
67 |
68 | }
69 |
70 | private string[] axis = new[] { "X", "Y", "Z" };
71 |
72 | public void Generate()
73 | {
74 | AnimatorController mergedController = null;
75 |
76 | syncObjects = syncObjects.Where(x => x != null).ToArray();
77 |
78 | VRCAvatarDescriptor descriptor = syncObjects.Select(x => x.GetComponentInParent()).FirstOrDefault();
79 |
80 | if (descriptor == null) return;
81 |
82 | int syncSteps = Mathf.CeilToInt(GetMaxBitCount() / (float)bitCount);
83 | int syncStepParameterCount = Mathf.CeilToInt(Mathf.Log(syncSteps, 2));
84 | bool[][] syncStepPerms = GeneratePermutations(syncStepParameterCount);
85 | int objectCount = syncObjects.Count(x => x != null);
86 | int objectParameterCount = Mathf.CeilToInt(Mathf.Log(objectCount, 2));
87 | bool[][] objectPerms = GeneratePermutations(objectParameterCount);
88 |
89 | #region Resource Setup
90 |
91 | descriptor.customizeAnimationLayers = true;
92 | RuntimeAnimatorController runtimeController = descriptor.baseAnimationLayers
93 | .Where(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX).Select(x => x.animatorController)
94 | .FirstOrDefault();
95 | AnimatorController mergeController = runtimeController == null ? null : (AnimatorController) runtimeController;
96 |
97 | VRCExpressionParameters parameterObject = descriptor.expressionParameters;
98 | VRCExpressionsMenu menuObject = GetMenuFromLocation(descriptor, menuLocation);
99 |
100 | Directory.CreateDirectory(STANDARD_NEW_ANIMATOR_FOLDER);
101 | string uniqueControllerPath = AssetDatabase.GenerateUniqueAssetPath(STANDARD_NEW_ANIMATOR_FOLDER + "CustomObjectSync.controller");
102 | if (mergeController == null)
103 | {
104 | mergeController = new AnimatorController();
105 | AssetDatabase.CreateAsset(mergeController, uniqueControllerPath);
106 | }
107 | else
108 | {
109 | AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(mergeController), uniqueControllerPath);
110 | }
111 |
112 | AssetDatabase.SaveAssets();
113 | AssetDatabase.Refresh();
114 | mergedController = AssetDatabase.LoadAssetAtPath(uniqueControllerPath);
115 |
116 | if (mergedController == null)
117 | {
118 | Debug.LogError("Creation of Controller object failed. Please report this to jellejurre on the VRLabs discord at discord.vrlabs.dev");
119 | return;
120 | }
121 |
122 | if (parameterObject == null)
123 | {
124 | Directory.CreateDirectory(STANDARD_NEW_PARAMASSET_FOLDER);
125 | string uniquePath = AssetDatabase.GenerateUniqueAssetPath(STANDARD_NEW_PARAMASSET_FOLDER + "Parameters.asset");
126 | parameterObject = CreateInstance();
127 | parameterObject.parameters = Array.Empty();
128 | AssetDatabase.CreateAsset(parameterObject, uniquePath);
129 | AssetDatabase.SaveAssets();
130 | AssetDatabase.Refresh();
131 | parameterObject = AssetDatabase.LoadAssetAtPath(uniquePath);
132 | }
133 |
134 | if (parameterObject == null)
135 | {
136 | Debug.LogError("Creation of Parameter object failed. Please report this to jellejurre on the VRLabs discord at discord.vrlabs.dev");
137 | return;
138 | }
139 |
140 | if (menuObject == null)
141 | {
142 | Directory.CreateDirectory(STANDARD_NEW_MENUASSET_FOLDER);
143 | string uniquePath = AssetDatabase.GenerateUniqueAssetPath(STANDARD_NEW_MENUASSET_FOLDER + "Menu.asset");
144 | menuObject = CreateInstance();
145 | menuObject.controls = new List();
146 | AssetDatabase.CreateAsset(menuObject, uniquePath);
147 | AssetDatabase.SaveAssets();
148 | AssetDatabase.Refresh();
149 | menuObject = AssetDatabase.LoadAssetAtPath(uniquePath);
150 | descriptor.expressionsMenu = menuObject;
151 | }
152 |
153 | if (menuObject == null)
154 | {
155 | Debug.LogError("Creation of Menu object failed. Please report this to jellejurre on the VRLabs discord at discord.vrlabs.dev");
156 | return;
157 | }
158 |
159 | AnimationClip buffer = GenerateClip("Buffer");
160 | AddCurve(buffer, "THIS PATH DOES NOT EXIST", typeof(Object), "NOPE", AnimationCurve.Constant(0, 1/60f, 0));
161 |
162 | #endregion
163 |
164 | int positionBits = maxRadius + positionPrecision;
165 | int rotationBits = rotationEnabled ? rotationPrecision : 0;
166 |
167 | #region Bit Parameters
168 |
169 | List parameters = new List()
170 | {
171 | GenerateBoolParameter("IsLocal"),
172 | GenerateFloatParameter("CustomObjectSync/One", defaultFloat: 1f),
173 | GenerateFloatParameter("CustomObjectSync/AngleMagX_Angle"),
174 | GenerateFloatParameter("CustomObjectSync/AngleMagY_Angle"),
175 | GenerateFloatParameter("CustomObjectSync/AngleMagZ_Angle"),
176 | GenerateFloatParameter("CustomObjectSync/AngleSignX_Angle"),
177 | GenerateFloatParameter("CustomObjectSync/AngleSignY_Angle"),
178 | GenerateFloatParameter("CustomObjectSync/AngleSignZ_Angle"),
179 | GenerateFloatParameter("CustomObjectSync/RotationX"),
180 | GenerateFloatParameter("CustomObjectSync/RotationY"),
181 | GenerateFloatParameter("CustomObjectSync/RotationZ"),
182 | GenerateFloatParameter("CustomObjectSync/PositionX"),
183 | GenerateFloatParameter("CustomObjectSync/PositionY"),
184 | GenerateFloatParameter("CustomObjectSync/PositionZ"),
185 | GenerateFloatParameter("CustomObjectSync/PositionSignX"),
186 | GenerateFloatParameter("CustomObjectSync/PositionSignY"),
187 | GenerateFloatParameter("CustomObjectSync/PositionSignZ"),
188 | GenerateFloatParameter("CustomObjectSync/PositionXPos"),
189 | GenerateFloatParameter("CustomObjectSync/PositionXNeg"),
190 | GenerateFloatParameter("CustomObjectSync/PositionYPos"),
191 | GenerateFloatParameter("CustomObjectSync/PositionYNeg"),
192 | GenerateFloatParameter("CustomObjectSync/PositionZPos"),
193 | GenerateFloatParameter("CustomObjectSync/PositionZNeg"),
194 | GenerateBoolParameter("CustomObjectSync/SetStage"),
195 | GenerateBoolParameter("CustomObjectSync/Enabled")
196 | };
197 |
198 |
199 | if (!quickSync)
200 | {
201 | AddBitConversionParameters(positionBits, parameters, objectCount, rotationBits);
202 | }
203 |
204 | for (int b = 0; b < objectParameterCount; b++)
205 | {
206 | parameters.Add(GenerateFloatParameter($"CustomObjectSync/LocalReadBit{b}"));
207 | }
208 |
209 | if (addLocalDebugView)
210 | {
211 | parameters.Add(GenerateBoolParameter("CustomObjectSync/LocalDebugView", false));
212 | parameters = parameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/LocalDebugView/Position{x}", 0f))).ToList();
213 | parameters = parameters.Concat(axis.Select(x => GenerateIntParameter($"CustomObjectSync/LocalDebugView/PositionTmp{x}", 0))).ToList();
214 | if (quickSync)
215 | {
216 | parameters = parameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/LocalDebugView/PositionSign{x}", 0f))).ToList();
217 | parameters = parameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/LocalDebugView/PositionTmp2{x}", 0))).ToList();
218 | }
219 | if (rotationEnabled)
220 | {
221 | parameters = parameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/LocalDebugView/Rotation{x}", 0f))).ToList();
222 | parameters = parameters.Concat(axis.Select(x => GenerateIntParameter($"CustomObjectSync/LocalDebugView/RotationTmp{x}", 0))).ToList();
223 | }
224 | }
225 |
226 | mergedController.parameters = mergedController.parameters.Concat(parameters.Where(p => mergedController.parameters.All(x => x.name != p.name))).ToArray();
227 |
228 | SetupSyncLayerParameters(syncSteps, objectCount, objectParameterCount, syncStepParameterCount, syncStepPerms, mergedController);
229 | ControllerGenerationMethods.defaultWriteDefaults = writeDefaults;
230 |
231 | #endregion
232 |
233 | #region VRCParameters
234 | List parameterList = new List();
235 | parameterList.Add(GenerateVRCParameter("CustomObjectSync/Enabled", VRCExpressionParameters.ValueType.Bool));
236 | if (quickSync)
237 | {
238 | parameterList = parameterList.Concat(axis.Select(x => GenerateVRCParameter($"CustomObjectSync/Sync/Position{x}", VRCExpressionParameters.ValueType.Float))).ToList();
239 | parameterList = parameterList.Concat(axis.Select(x => GenerateVRCParameter($"CustomObjectSync/Sync/PositionSign{x}", VRCExpressionParameters.ValueType.Bool))).ToList();
240 | if (rotationEnabled)
241 | {
242 | parameterList = parameterList.Concat(axis.Select(x => GenerateVRCParameter($"CustomObjectSync/Sync/Rotation{x}", VRCExpressionParameters.ValueType.Float))).ToList();
243 | }
244 | }
245 | else
246 | {
247 | for (int b = 0; b < bitCount; b++)
248 | {
249 | parameterList.Add(GenerateVRCParameter($"CustomObjectSync/Sync/Data{b}", VRCExpressionParameters.ValueType.Bool));
250 | }
251 |
252 | for (int b = 0; b < syncStepParameterCount; b++)
253 | {
254 | parameterList.Add(GenerateVRCParameter($"CustomObjectSync/Sync/Step{b}", VRCExpressionParameters.ValueType.Bool, defaultValue: syncStepPerms[syncSteps-1][b] ? 1 : 0));
255 | }
256 | }
257 |
258 |
259 | for (int b = 0; b < objectParameterCount; b++)
260 | {
261 | parameterList.Add(GenerateVRCParameter($"CustomObjectSync/Sync/Object{b}", VRCExpressionParameters.ValueType.Bool));
262 | }
263 |
264 | if (addLocalDebugView)
265 | {
266 | parameterList.Add(GenerateVRCParameter("CustomObjectSync/LocalDebugView", VRCExpressionParameters.ValueType.Bool, networkSynced: false));
267 | }
268 |
269 |
270 | parameterObject.parameters = parameterObject.parameters.Concat(parameterList).ToArray();
271 |
272 | menuObject.controls.Add(new VRCExpressionsMenu.Control()
273 | {
274 | name = "Enable Custom Object Sync",
275 | style = VRCExpressionsMenu.Control.Style.Style1,
276 | type = VRCExpressionsMenu.Control.ControlType.Toggle,
277 | parameter = new VRCExpressionsMenu.Control.Parameter()
278 | {
279 | name = "CustomObjectSync/Enabled"
280 | }
281 | });
282 |
283 | if (addLocalDebugView)
284 | {
285 | menuObject.controls.Add(new VRCExpressionsMenu.Control()
286 | {
287 | name = "Show Remote Position",
288 | style = VRCExpressionsMenu.Control.Style.Style1,
289 | type = VRCExpressionsMenu.Control.ControlType.Toggle,
290 | parameter = new VRCExpressionsMenu.Control.Parameter()
291 | {
292 | name = "CustomObjectSync/LocalDebugView"
293 | }
294 | });
295 | }
296 |
297 | EditorUtility.SetDirty(menuObject);
298 | EditorUtility.SetDirty(parameterObject);
299 | #endregion
300 |
301 | GameObject syncSystem = InstallSystem(descriptor, mergedController, parameterObject);
302 |
303 | List layersToAdd = new List();
304 |
305 | if (!quickSync) layersToAdd = GenerateBitConversionLayers(objectCount, buffer, positionBits, objectParameterCount, rotationBits);
306 |
307 | AnimatorControllerLayer syncLayer = SetupSyncLayer(syncSteps, positionBits, rotationBits, objectCount, objectParameterCount, objectPerms, syncStepParameterCount, syncStepPerms);
308 | layersToAdd.Add(syncLayer);
309 |
310 | AnimatorControllerLayer displayLayer = SetupDisplayLayer(descriptor, objectCount, syncSystem, buffer, objectParameterCount, objectPerms);
311 | layersToAdd.Add(displayLayer);
312 |
313 | if (addLocalDebugView)
314 | {
315 | layersToAdd.Add(SetupLocalDebugViewLayer(descriptor, objectCount, syncSystem, buffer, objectParameterCount, objectPerms));
316 | }
317 |
318 | mergedController.layers = mergedController.layers.Concat(layersToAdd).ToArray();
319 |
320 | foreach (AnimatorState state in mergedController.layers.Where(x => x.name.Contains("CustomObjectSync")).SelectMany(x => x.stateMachine.states).Select(x => x.state))
321 | {
322 | if (state.motion is null)
323 | {
324 | state.motion = buffer;
325 | }
326 | }
327 | Directory.CreateDirectory(STANDARD_NEW_ANIMATION_FOLDER);
328 |
329 | try
330 | {
331 | AssetDatabase.StartAssetEditing();
332 | SerializeController(mergedController);
333 | foreach (var clip in mergedController.animationClips)
334 | {
335 | if (String.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip))){
336 | if (String.IsNullOrEmpty(clip.name))
337 | {
338 | clip.name = "Anim";
339 | }
340 | var uniqueFileName = AssetDatabase.GenerateUniqueAssetPath($"{STANDARD_NEW_ANIMATION_FOLDER}{clip.name}.anim");
341 | AssetDatabase.CreateAsset(clip, uniqueFileName);
342 | }
343 | }
344 | }
345 | finally
346 | {
347 | AssetDatabase.StopAssetEditing();
348 | }
349 |
350 |
351 | EditorUtility.DisplayDialog("Success!", "Custom Object Sync has been successfully installed", "Ok");
352 | }
353 |
354 | private AnimatorControllerLayer SetupDisplayLayer(VRCAvatarDescriptor descriptor, int objectCount,
355 | GameObject syncSystem, AnimationClip buffer, int objectParameterCount, bool[][] objectPerms)
356 | {
357 | string[] targetStrings = syncObjects.Select(x => AnimationUtility.CalculateTransformPath(addDampeningConstraint ? x.transform.parent.Find($"{x.name} Damping Sync") : x.transform, descriptor.transform)).ToArray();
358 | string[] dampingConstraints = syncObjects.Select(x => AnimationUtility.CalculateTransformPath(x.transform, descriptor.transform)).ToArray();
359 | float contactBugOffset = Mathf.Pow(2, maxRadius - 6); // Fixes a bug where proximity contacts near edges give 0, so we set this theoretical 0 to far away from spawn to reduce chances of this happening
360 |
361 | AnimationClip enableMeasure = GenerateClip("localMeasureEnabled");
362 | AddCurve(enableMeasure, "Custom Object Sync/Measure", typeof(GameObject), "m_IsActive", AnimationCurve.Constant(0, 1/60f, 1));
363 | AddContactCurves(enableMeasure, AnimationCurve.Constant(0, 1f, contactBugOffset / Mathf.Pow(2, maxRadius) * 3));
364 |
365 | AnimationClip remoteVRCParentConstraintOff = GenerateClip("remoteVRCParentConstraintDisabled");
366 | AddCurve(remoteVRCParentConstraintOff, "Custom Object Sync/Measure", typeof(GameObject), "m_IsActive", AnimationCurve.Constant(0, 1/60f, 0));
367 | AddCurve(remoteVRCParentConstraintOff, "Custom Object Sync/Set", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 1/60f, 1));
368 |
369 | for (var i = 0; i < targetStrings.Length; i++)
370 | {
371 | var targetString = targetStrings[i];
372 | AddCurve(remoteVRCParentConstraintOff, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1 / 60f, 1));
373 | AddCurve(remoteVRCParentConstraintOff, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1 / 60f, 0));
374 | if (addDampeningConstraint)
375 | {
376 | AddCurve(remoteVRCParentConstraintOff, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1 / 60f, 1));
377 | AddCurve(remoteVRCParentConstraintOff, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1 / 60f, 0));
378 | }
379 | AddCurve(remoteVRCParentConstraintOff, targetString, typeof(VRCParentConstraint), "m_Enabled", AnimationCurve.Constant(0, 1 / 60f, 1));
380 | }
381 |
382 | AnimationClip disableDamping = GenerateClip("localDisableDamping");
383 | if (addDampeningConstraint)
384 | {
385 | foreach (string targetPath in syncObjects.Select(x => AnimationUtility.CalculateTransformPath(x.transform, descriptor.transform)))
386 | {
387 | AddCurve(disableDamping, targetPath, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 1));
388 | AddCurve(disableDamping, targetPath, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 0));
389 | }
390 | }
391 |
392 | ChildAnimatorState StateIdleRemote = GenerateChildState(new Vector3(30f, 180f, 0f), GenerateState("Idle/Remote"));
393 | bool[][] axisPermutations = GeneratePermutations(3);
394 | List displayStates = new List();
395 | AnimationClip[] localConstraintTargetClips = Enumerable.Range(0, objectCount).Select(x =>
396 | {
397 | string targetString = AnimationUtility.CalculateTransformPath(syncSystem.transform, descriptor.transform) + "/Target";
398 | AnimationClip localConstraintOn = GenerateClip($"localVRCParentConstraintEnabled{x}");
399 | Enumerable.Range(0, objectCount).ToList().ForEach(y=> AddCurve(localConstraintOn, targetString, typeof(VRCParentConstraint), $"Sources.source{y}.Weight", AnimationCurve.Constant(0, 1 / 60f, x == y ? 1 : 0)));
400 | return localConstraintOn;
401 | }).ToArray();
402 | BlendTree localEnableTree = GenerateBlendTree("LocalSetTree", BlendTreeType.Direct);
403 |
404 | Motion RecurseCreateTree(int depth, int index, int max)
405 | {
406 | if (depth == 0)
407 | {
408 | return index >= localConstraintTargetClips.Length ? buffer : localConstraintTargetClips[index];
409 | }
410 |
411 | BlendTree childTree = GenerateBlendTree($"TargetTree{depth}-{index}", BlendTreeType.Simple1D, blendParameter: $"CustomObjectSync/LocalReadBit{depth - 1}");
412 | childTree.children = new[]
413 | {
414 | GenerateChildMotion(RecurseCreateTree(depth - 1, index * 2, max)),
415 | GenerateChildMotion(RecurseCreateTree(depth - 1, index * 2 + 1, max))
416 | };
417 | return childTree;
418 | }
419 |
420 | Motion initialTargetTree = RecurseCreateTree(objectParameterCount, 0,(int)Math.Pow(2, objectParameterCount));
421 | localEnableTree.children = new[]
422 | {
423 | GenerateChildMotion(enableMeasure, directBlendParameter: "CustomObjectSync/One"),
424 | GenerateChildMotion(disableDamping, directBlendParameter: "CustomObjectSync/One"),
425 | GenerateChildMotion(initialTargetTree, directBlendParameter: "CustomObjectSync/One")
426 | };
427 |
428 | for (var p = 0; p < axisPermutations.Length; p++)
429 | {
430 | bool[] perm = axisPermutations[p];
431 | AnimatorState stateRot = GenerateState($"X{(perm[0] ? "+" : "-")}Y{(perm[1] ? "+" : "-")}Z{(perm[2] ? "+" : "-")}Rot", motion: localEnableTree, writeDefaultValues: true);
432 | stateRot.behaviours = new StateMachineBehaviour[] {
433 | GenerateParameterDriver(
434 | Enumerable.Range(0, axis.Length).Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/AngleMag{axis[x]}_Angle", name: $"CustomObjectSync/Rotation{axis[x]}",
435 | convertRange: true, destMax: perm[x] ? 1f : 0f, destMin: 0.5f, sourceMax: 1f)
436 | ).Append(GenerateParameter(ChangeType.Set, name: "CustomObjectSync/SetStage", value: 1f)).ToArray())
437 | };
438 | displayStates.Add(GenerateChildState(new Vector3(-220f, -210f + 60 * p, 0f), stateRot));
439 |
440 | AnimatorState statePos = GenerateState($"X{(perm[0] ? "+" : "-")}Y{(perm[1] ? "+" : "-")}Z{(perm[2] ? "+" : "-")}Pos", motion: localEnableTree, writeDefaultValues: true);
441 | if (quickSync)
442 | {
443 | statePos.behaviours = new StateMachineBehaviour[] {
444 | GenerateParameterDriver(
445 | Enumerable.Range(0, axis.Length).Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Position{axis[x]}{(perm[x] ? "Pos" : "Neg")}", name: $"CustomObjectSync/Position{axis[x]}"))
446 | .Concat(Enumerable.Range(0, axis.Length).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/PositionSign{axis[x]}", value: perm[x] ? 1.0f : 0.0f))).Append(GenerateParameter(ChangeType.Set, name: "CustomObjectSync/SetStage", value: 0f)).ToArray())
447 | };
448 | }
449 | else
450 | {
451 | statePos.behaviours = new StateMachineBehaviour[] {
452 | GenerateParameterDriver(
453 | Enumerable.Range(0, axis.Length).Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Position{axis[x]}{(perm[x] ? "Pos" : "Neg")}", name: $"CustomObjectSync/Position{axis[x]}",
454 | convertRange: true, destMax: perm[x] ? 1f : 0f, destMin: 0.5f, sourceMax: 1f)
455 | ).Append(GenerateParameter(ChangeType.Set, name: "CustomObjectSync/SetStage", value: 0f)).ToArray())
456 | };
457 | }
458 | displayStates.Add(GenerateChildState(new Vector3(-470f, -210f + 60 * p, 0f), statePos));
459 |
460 | }
461 | void AddContactCurves(AnimationClip clip, AnimationCurve curve)
462 | {
463 | AddCurve(clip, "Custom Object Sync/Measure/Position/SenderX", typeof(VRCPositionConstraint), "PositionOffset.x", curve);
464 | AddCurve(clip, "Custom Object Sync/Measure/Position/SenderY", typeof(VRCPositionConstraint), "PositionOffset.y", curve);
465 | AddCurve(clip, "Custom Object Sync/Measure/Position/SenderZ", typeof(VRCPositionConstraint), "PositionOffset.z", curve);
466 | }
467 |
468 | AnimationClip ContactTimeoutClip = GenerateClip("ContactTimeout");
469 | AddContactCurves(ContactTimeoutClip, GenerateCurve(new[] { GenerateKeyFrame(0, contactBugOffset / Mathf.Pow(2, maxRadius) * 3), GenerateKeyFrame(0.1f, 10), GenerateKeyFrame(0.2f, contactBugOffset / Mathf.Pow(2, maxRadius) * 3), GenerateKeyFrame(0.3f, contactBugOffset / Mathf.Pow(2, maxRadius) * 3) }));
470 |
471 |
472 | BlendTree ContactTimeoutTree = GenerateBlendTree("ContactTimeout", BlendTreeType.Direct);
473 | ContactTimeoutTree.children = new ChildMotion[]
474 | {
475 | GenerateChildMotion(motion: localEnableTree, directBlendParameter: "CustomObjectSync/One"),
476 | GenerateChildMotion(motion: ContactTimeoutClip, directBlendParameter: "CustomObjectSync/One")
477 | };
478 | ChildAnimatorState ContactTimeoutState = GenerateChildState(new Vector3(-470f, -270f, 0f), GenerateState("ContactTimeout", motion: ContactTimeoutTree, writeDefaultValues: true));
479 | List remoteOnStates = new List();
480 | AnimationClip[] constraintsEnabled = Enumerable.Range(0, targetStrings.Length).Select(i =>
481 | {
482 | string targetString = targetStrings[i];
483 | AnimationClip remoteVRCParentConstraintOn = GenerateClip($"remoteVRCParentConstraintEnabled{i}");
484 | AddCurve(remoteVRCParentConstraintOn, "Custom Object Sync/Measure", typeof(GameObject), "m_IsActive", AnimationCurve.Constant(0, 1/60f, 0));
485 | AddCurve(remoteVRCParentConstraintOn, "Custom Object Sync/Set", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 1/60f, 0));
486 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "m_Enabled", AnimationCurve.Linear(0, 0, 2/60f, 1));
487 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 0));
488 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
489 | if (addDampeningConstraint)
490 | {
491 | AddCurve(remoteVRCParentConstraintOn, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, dampingConstraintValue));
492 | AddCurve(remoteVRCParentConstraintOn, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
493 | }
494 | return remoteVRCParentConstraintOn;
495 | }).ToArray();
496 | AnimationClip[] constraintsDisabled = Enumerable.Range(0, targetStrings.Length).Select(i =>
497 | {
498 | string targetString = targetStrings[i];
499 | AnimationClip remoteVRCParentConstraintOffAnim = GenerateClip($"remoteVRCParentConstraintDisabled{i}");
500 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "m_Enabled", AnimationCurve.Constant(0, 1/60f, 0));
501 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 0));
502 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
503 | if (addDampeningConstraint)
504 | {
505 | AddCurve(remoteVRCParentConstraintOffAnim, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 1));
506 | AddCurve(remoteVRCParentConstraintOffAnim, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 0));
507 | }
508 | return remoteVRCParentConstraintOffAnim;
509 | }).ToArray();
510 |
511 | for (int o = 0; o < objectCount; o++)
512 | {
513 | AnimatorState remoteOnState = GenerateState("Remote On", writeDefaultValues: true);
514 | var remoteTree = GenerateSetterTree(false);
515 | remoteTree.children = remoteTree.children.Concat(Enumerable.Range(0, objectCount).Select(o2 => GenerateChildMotion(o2 == o ? constraintsEnabled[o2] : constraintsDisabled[o2], directBlendParameter: "CustomObjectSync/One"))).ToArray();
516 |
517 | remoteOnState.motion = remoteTree;
518 | displayStates.Add(GenerateChildState(new Vector3(260f, -60f * (o + 1), 0f), remoteOnState));
519 | remoteOnStates.Add(displayStates.Last());
520 | }
521 |
522 | AnimatorState StateRemoteOff = GenerateState("Remote Off", motion: remoteVRCParentConstraintOff);
523 | displayStates.Add(GenerateChildState(new Vector3(260f, 0, 0f), StateRemoteOff));
524 |
525 | AnimatorStateMachine displayStateMachine = GenerateStateMachine("CustomObjectSync/Parameter Setup and Display", new Vector3(50f, 20f, 0f), new Vector3(50f, 120f, 0f), new Vector3(800f, 120f, 0f), states: displayStates.Append(ContactTimeoutState).ToArray(), defaultState: StateIdleRemote.state);
526 |
527 | List anyStateTransitions = new List();
528 | anyStateTransitions.Add(GenerateTransition("", conditions:
529 | new AnimatorCondition[]
530 | {
531 | GenerateCondition(AnimatorConditionMode.IfNot, "IsLocal", 0f),
532 | GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/Enabled", 0f)
533 | }, destinationState: StateRemoteOff));
534 |
535 | string objParam = quickSync ? "CustomObjectSync/Sync/Object" : "CustomObjectSync/Object";
536 | for (var o = 0; o < remoteOnStates.Count; o++)
537 | {
538 | int index = quickSync ? (o + 1) % objectCount : o;
539 | anyStateTransitions.Add(GenerateTransition("", conditions:
540 | Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[index][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"{objParam}{x}", threshold: 0))
541 | .Append(GenerateCondition(AnimatorConditionMode.IfNot, "IsLocal", 0f))
542 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0f)).ToArray()
543 | , destinationState: remoteOnStates[o].state));
544 | }
545 |
546 |
547 | for (int p = 0; p < axisPermutations.Length; p++)
548 | {
549 | bool[] perm = axisPermutations[p];
550 | anyStateTransitions.Add(
551 | GenerateTransition("", canTransitionToSelf: true, conditions:
552 | Enumerable.Range(0, axis.Length).Select(x =>
553 | GenerateCondition(perm[x] ? AnimatorConditionMode.Less : AnimatorConditionMode.Greater,
554 | $"CustomObjectSync/AngleSign{axis[x]}_Angle", perm[x] ? 0.5000001f : 0.5f))
555 | .Append(GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f))
556 | .Append(GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/SetStage", 0f)).ToArray()
557 | , destinationState: displayStates[2 * p].state)
558 | );
559 | anyStateTransitions.Add(
560 | GenerateTransition("", canTransitionToSelf: true, conditions:
561 | Enumerable.Range(0, axis.Length).SelectMany(x => new []
562 | {
563 | GenerateCondition(AnimatorConditionMode.Greater,
564 | $"CustomObjectSync/Position{axis[x]}{(perm[x] ? "Pos" : "Neg")}", 0),
565 | GenerateCondition(AnimatorConditionMode.Less,
566 | $"CustomObjectSync/Position{axis[x]}{(!perm[x] ? "Pos" : "Neg")}", 0.00000001f),
567 | })
568 | .Append(GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f))
569 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/SetStage", 0f)).ToArray()
570 | , destinationState: displayStates[2 * p + 1].state)
571 | );
572 | }
573 |
574 |
575 | anyStateTransitions.Add(GenerateTransition("", conditions:
576 | new [] { GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f),
577 | GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/SetStage", 0f)}
578 | , destinationState: ContactTimeoutState.state, hasExitTime: true, exitTime: 1f));
579 |
580 | displayStateMachine.anyStateTransitions = anyStateTransitions.ToArray().Reverse().ToArray();
581 |
582 | AnimatorControllerLayer displayLayer = GenerateLayer("CustomObjectSync/Parameter Display", displayStateMachine);
583 | return displayLayer;
584 | }
585 |
586 | private BlendTree GenerateSetterTree(bool isDebugTree)
587 | {
588 | BlendTree tree = GenerateBlendTree("RemoteTree", BlendTreeType.Direct);
589 |
590 | BlendTree[] localTrees = axis.Select(x =>
591 | {
592 | AnimationClip rotationXMin = GenerateClip($"Rotation{x}Min");
593 | AddCurve(rotationXMin, "Custom Object Sync/Set/Result", typeof(VRCRotationConstraint), $"RotationOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, isDebugTree ? -180 : -180 - (360 / (float)Math.Pow(2, rotationPrecision)))) ;
594 | AnimationClip rotationXMax = GenerateClip($"Rotation{x}Max");
595 | AddCurve(rotationXMax, "Custom Object Sync/Set/Result", typeof(VRCRotationConstraint), $"RotationOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, isDebugTree ? 180 : 180 - (360 / (float)Math.Pow(2, rotationPrecision))));
596 | BlendTree rotationTree = GenerateBlendTree($"Rotation{x}", BlendTreeType.Simple1D,
597 | blendParameter: isDebugTree ? $"CustomObjectSync/LocalDebugView/Rotation{x}" : $"CustomObjectSync/Rotation{x}");
598 | rotationTree.children = new ChildMotion[]
599 | {
600 | GenerateChildMotion(motion: rotationXMin),
601 | GenerateChildMotion(motion: rotationXMax)
602 | };
603 | return rotationTree;
604 | }).ToArray();
605 |
606 |
607 | if (quickSync)
608 | {
609 | localTrees = localTrees.Concat(axis.Select(x =>
610 | {
611 | float value = Mathf.Pow(2, maxRadius);
612 | float offset = 1 / Mathf.Pow(2, positionPrecision + 1);
613 | AnimationClip translationMin = GenerateClip($"Translation{x}Min");
614 | AddCurve(translationMin, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, -offset - value));
615 | AnimationClip translationZeroNeg = GenerateClip($"Translation{x}ZeroNeg");
616 | AddCurve(translationZeroNeg, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, -offset));
617 | AnimationClip translationZeroPos = GenerateClip($"Translation{x}ZeroPos");
618 | AddCurve(translationZeroPos, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, offset));
619 | AnimationClip translationMax = GenerateClip($"Translation{x}Max");
620 | AddCurve(translationMax, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, offset + value));
621 | BlendTree translationTree = GenerateBlendTree("TranslationX", BlendTreeType.Simple1D, blendParameter: isDebugTree ? $"CustomObjectSync/LocalDebugView/PositionSign{x}" : $"CustomObjectSync/PositionSign{x}");
622 |
623 | BlendTree translationMinTree = GenerateBlendTree($"Translation{x}MinTree", blendType: BlendTreeType.Simple1D, blendParameter:isDebugTree ? $"CustomObjectSync/LocalDebugView/Position{x}" : $"CustomObjectSync/Position{x}");
624 | BlendTree translationMaxTree = GenerateBlendTree($"Translation{x}MaxTree", blendType: BlendTreeType.Simple1D, blendParameter:isDebugTree ? $"CustomObjectSync/LocalDebugView/Position{x}" : $"CustomObjectSync/Position{x}");
625 | translationMinTree.children = new[]
626 | {
627 | GenerateChildMotion(motion: translationZeroNeg),
628 | GenerateChildMotion(motion: translationMin)
629 | };
630 | translationMaxTree.children = new[]
631 | {
632 | GenerateChildMotion(motion: translationZeroPos),
633 | GenerateChildMotion(motion: translationMax)
634 | };
635 | translationTree.children = new ChildMotion[]
636 | {
637 | GenerateChildMotion(translationMinTree),
638 | GenerateChildMotion(translationMaxTree)
639 | };
640 | return translationTree;
641 | })).ToArray();
642 | }
643 | else
644 | {
645 | localTrees = localTrees.Concat(axis.Select(x =>
646 | {
647 | AnimationClip translationMin = GenerateClip($"Translation{x}Min");
648 | AddCurve(translationMin, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, -Mathf.Pow(2, maxRadius)));
649 | AnimationClip translationMax = GenerateClip($"Translation{x}Max");
650 | AddCurve(translationMax, "Custom Object Sync/Set/Result", typeof(VRCPositionConstraint), $"PositionOffset.{x.ToLower()}", AnimationCurve.Constant(0, 1/60f, Mathf.Pow(2, maxRadius)));
651 | BlendTree translationTree = GenerateBlendTree($"Translation{x}", BlendTreeType.Simple1D,
652 | blendParameter: isDebugTree ? $"CustomObjectSync/LocalDebugView/Position{x}" : $"CustomObjectSync/Position{x}");
653 | translationTree.children = new ChildMotion[]
654 | {
655 | GenerateChildMotion(motion: translationMin),
656 | GenerateChildMotion(motion: translationMax)
657 | };
658 | return translationTree;
659 | })).ToArray();
660 | }
661 |
662 | tree.children = localTrees.Select(x => GenerateChildMotion(motion: x, directBlendParameter: "CustomObjectSync/One")).ToArray();
663 | return tree;
664 | }
665 |
666 | private AnimatorControllerLayer SetupLocalDebugViewLayer(VRCAvatarDescriptor descriptor, int objectCount,
667 | GameObject syncSystem, AnimationClip buffer, int objectParameterCount, bool[][] objectPerms)
668 | {
669 | List states = new List();
670 | AnimatorState StateIdleRemote = GenerateState("Idle Remote", motion: buffer);
671 | ChildAnimatorState IdleRemote = GenerateChildState(new Vector3(30f, 200f, 0f), StateIdleRemote);
672 | states.Add(IdleRemote);
673 |
674 | string[] targetStrings = syncObjects.Select(x => AnimationUtility.CalculateTransformPath(addDampeningConstraint ? x.transform.parent.Find($"{x.name} Damping Sync") : x.transform, descriptor.transform)).ToArray();
675 | string[] dampingConstraints = syncObjects.Select(x => AnimationUtility.CalculateTransformPath(x.transform, descriptor.transform)).ToArray();
676 |
677 | AnimationClip idleLocal = GenerateClip("IdleLocal");
678 | Enumerable.Range(0, targetStrings.Length).ToList().ForEach(i =>
679 | {
680 | string targetString = targetStrings[i];
681 | AnimationCurve enabledCurve = AnimationCurve.Constant(0, 1/60f, 1);
682 | AddCurve(idleLocal, targetString, typeof(VRCParentConstraint), "m_Enabled",enabledCurve);
683 | AddCurve(idleLocal, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 1));
684 | AddCurve(idleLocal, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 0));
685 | if (addDampeningConstraint)
686 | {
687 | AddCurve(idleLocal, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 1));
688 | AddCurve(idleLocal, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 0));
689 | }
690 | });
691 |
692 | AnimatorState StateIdleLocal = GenerateState("Idle Local", motion: idleLocal);
693 | ChildAnimatorState IdleLocal = GenerateChildState(new Vector3(30f, -80f, 0f), StateIdleLocal);
694 | states.Add(IdleLocal);
695 |
696 | AnimationClip[] constraintsEnabled = Enumerable.Range(0, targetStrings.Length).Select(i =>
697 | {
698 | string targetString = targetStrings[i];
699 | AnimationClip remoteVRCParentConstraintOn = GenerateClip($"remoteVRCParentConstraintEnabled{i}");
700 | AnimationCurve enabledCurve = quickSync ? new AnimationCurve(new Keyframe(0, 0), new Keyframe(6 / 60f, 0), new Keyframe(7 / 60f, 1), new Keyframe(8 / 60f, 0)) :
701 | new AnimationCurve(new Keyframe(0, 0), new Keyframe(1 / 60f, 1), new Keyframe(2 / 60f, 0));
702 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "m_Enabled",enabledCurve);
703 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 0));
704 | AddCurve(remoteVRCParentConstraintOn, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
705 | if (addDampeningConstraint)
706 | {
707 | AddCurve(remoteVRCParentConstraintOn, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, dampingConstraintValue));
708 | AddCurve(remoteVRCParentConstraintOn, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
709 | }
710 | return remoteVRCParentConstraintOn;
711 | }).ToArray();
712 | AnimationClip[] constraintsDisabled = Enumerable.Range(0, targetStrings.Length).Select(i =>
713 | {
714 | string targetString = targetStrings[i];
715 | AnimationClip remoteVRCParentConstraintOffAnim = GenerateClip($"remoteVRCParentConstraintDisabled{i}");
716 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "m_Enabled", AnimationCurve.Constant(0, 1/60f, 0));
717 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, 0));
718 | AddCurve(remoteVRCParentConstraintOffAnim, targetString, typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
719 | if (addDampeningConstraint)
720 | {
721 | AddCurve(remoteVRCParentConstraintOffAnim, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source0.Weight", AnimationCurve.Constant(0, 1/60f, dampingConstraintValue));
722 | AddCurve(remoteVRCParentConstraintOffAnim, dampingConstraints[i], typeof(VRCParentConstraint), "Sources.source1.Weight", AnimationCurve.Constant(0, 1/60f, 1));
723 | }
724 | return remoteVRCParentConstraintOffAnim;
725 | }).ToArray();
726 |
727 | for (int i = 0; i < objectCount; i++)
728 | {
729 | BlendTree setPositionTree = GenerateSetterTree(true);
730 | BlendTree localTree = GenerateBlendTree($"LocalObject{i}", BlendTreeType.Direct);
731 | localTree.children = Enumerable.Range(0, objectCount)
732 | .Select(i2 =>
733 | {
734 | return GenerateChildMotion(motion: ((i2 + 1) % objectCount) == i ? constraintsEnabled[i2] : constraintsDisabled[i2], directBlendParameter: "CustomObjectSync/One");
735 | }).Append(GenerateChildMotion(motion: setPositionTree, directBlendParameter: "CustomObjectSync/One")).ToArray();
736 |
737 | AnimatorState localState = GenerateState($"Local Object {i}", motion: localTree, writeDefaultValues: true);
738 | localState.behaviours = new StateMachineBehaviour[] {
739 | GenerateParameterDriver(axis.SelectMany(x =>
740 | {
741 | List parameters = new List();
742 |
743 |
744 | if (quickSync)
745 | {
746 | float multiplier = (float)Math.Pow(2, maxRadius + positionPrecision - 1);
747 | Parameter pos1 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/PositionTmp2{x}", source: $"CustomObjectSync/Position{x}", convertRange: true, sourceMin: -1, sourceMax: 1, destMin: -multiplier, destMax: multiplier);
748 | Parameter pos2 = GenerateParameter(ChangeType.Add, name: $"CustomObjectSync/LocalDebugView/PositionTmp2{x}", value: 0.5f);
749 | Parameter pos3 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/PositionTmp{x}", source: $"CustomObjectSync/LocalDebugView/PositionTmp2{x}");
750 | Parameter pos4 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/Position{x}", source: $"CustomObjectSync/LocalDebugView/PositionTmp{x}", convertRange: true, sourceMin: -multiplier, sourceMax: multiplier, destMin: -1, destMax: 1);
751 | Parameter pos5 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/PositionSign{x}", source: $"CustomObjectSync/PositionSign{x}");
752 | parameters.Add(pos1);
753 | parameters.Add(pos2);
754 | parameters.Add(pos3);
755 | parameters.Add(pos4);
756 | parameters.Add(pos5);
757 | }
758 | else
759 | {
760 | float multiplier = (float)Math.Pow(2, maxRadius + positionPrecision);
761 | Parameter pos1 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/PositionTmp{x}", source: $"CustomObjectSync/Position{x}", convertRange: true, sourceMin: -1, sourceMax: 1, destMin: -multiplier, destMax: multiplier);
762 | Parameter pos2 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/Position{x}", source: $"CustomObjectSync/LocalDebugView/PositionTmp{x}", convertRange: true, sourceMin: -multiplier, sourceMax: multiplier, destMin: -1, destMax: 1);
763 | parameters.Add(pos1);
764 | parameters.Add(pos2);
765 | }
766 | if (rotationEnabled)
767 | {
768 | Parameter rot1 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/RotationTmp{x}", source: $"CustomObjectSync/Rotation{x}", convertRange: true, sourceMin: -1, sourceMax: 1, destMin: -(float)Math.Pow(2, rotationPrecision), destMax: (float)Math.Pow(2, rotationPrecision));
769 | Parameter rot2 = GenerateParameter(ChangeType.Copy, name: $"CustomObjectSync/LocalDebugView/Rotation{x}", source: $"CustomObjectSync/LocalDebugView/RotationTmp{x}", convertRange: true, sourceMin: -(float)Math.Pow(2, rotationPrecision), sourceMax: (float)Math.Pow(2, rotationPrecision), destMin: -1, destMax: 1);
770 | parameters.Add(rot1);
771 | parameters.Add(rot2);
772 | }
773 | return parameters.ToArray();
774 | }).ToArray())
775 | };
776 | ChildAnimatorState localChildState = GenerateChildState(new Vector3(30f - ((objectCount - 1)/2f * 300f) + i * 300, -180f, 0f), localState);
777 | states.Add(localChildState);
778 | }
779 |
780 | AnimatorStateTransition[] anyStateTransitions = Enumerable.Range(0, objectCount).Select(i =>
781 | {
782 | AnimatorStateTransition transition = GenerateTransition("", conditions:
783 | Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[i][x] ? AnimatorConditionMode.Greater : AnimatorConditionMode.Less, $"CustomObjectSync/LocalReadBit{x}", threshold: 0.5f))
784 | .Append(GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f))
785 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0f))
786 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/LocalDebugView", 0f)).ToArray()
787 | , destinationState: states[i+2].state);
788 | if (objectCount == 1)
789 | {
790 | transition.canTransitionToSelf = true;
791 |
792 | if (!quickSync)
793 | {
794 | transition.conditions = transition.conditions.Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/StartRead/0", 0f)).Append(GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/ReadInProgress/0", 0f)).ToArray();
795 | }
796 | else
797 | {
798 | BlendTree stateTree = states[i + 2].state.motion as BlendTree;
799 | AnimationClip lengthBuffer = GenerateClip("LengthDebugBuffer");
800 | AddCurve(lengthBuffer, "THIS OBJECT DOES NOT EXIST", typeof(GameObject), "m_Enabled", AnimationCurve.Constant(0, 12/60f, 1));
801 | stateTree.children = stateTree.children.Append(GenerateChildMotion(motion: lengthBuffer, directBlendParameter: "CustomObjectSync/One")).ToArray();
802 | transition.hasExitTime = true;
803 | transition.exitTime = 1f;
804 | }
805 | }
806 |
807 | return transition;
808 | }).Append(GenerateTransition("", conditions: new []
809 | {
810 | GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f),
811 | GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/LocalDebugView", 0f)
812 | }, destinationState: IdleLocal.state))
813 | .Append(GenerateTransition("", conditions: new []
814 | {
815 | GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0f),
816 | GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/Enabled", 0f)
817 | }, destinationState: IdleLocal.state))
818 | .Append(GenerateTransition("", conditions: new []
819 | {
820 | GenerateCondition(AnimatorConditionMode.IfNot, "IsLocal", 0f),
821 | }, destinationState: IdleRemote.state)).ToArray();
822 |
823 | AnimatorStateMachine debugStateMachine = GenerateStateMachine("CustomObjectSync/Local Debug View", new Vector3(50f, 20f, 0f), new Vector3(50f, 120f, 0f), new Vector3(800f, 120f, 0f), states: states.ToArray(), defaultState: IdleRemote.state, anyStateTransitions: anyStateTransitions);
824 |
825 | AnimatorControllerLayer localDebugViewLayer = GenerateLayer("CustomObjectSync/Local Debug View", debugStateMachine);
826 | return localDebugViewLayer;
827 | }
828 |
829 | // From https://stackoverflow.com/questions/38069770/add-one-gameobject-component-into-another-gameobject-with-values-at-runtime
830 | public static T CopyValues(Component target, T source) where T : Component
831 | {
832 | Type type = target.GetType();
833 | if (type != source.GetType()) return null; // type mis-match
834 | while (type != null && type != typeof(Behaviour))
835 | {
836 | BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance |
837 | BindingFlags.Default | BindingFlags.DeclaredOnly;
838 | PropertyInfo[] pinfos = type.GetProperties(flags);
839 | foreach (var pinfo in pinfos)
840 | {
841 | if (pinfo.CanWrite)
842 | {
843 | try
844 | {
845 | pinfo.SetValue(target, pinfo.GetValue(source, null), null);
846 | }
847 | catch
848 | {
849 | } // In case of NotImplementedException being thrown.
850 | }
851 | }
852 |
853 | FieldInfo[] finfos = type.GetFields(flags);
854 | foreach (var finfo in finfos)
855 | {
856 | finfo.SetValue(target, finfo.GetValue(source));
857 | }
858 |
859 | type = type.BaseType;
860 | }
861 |
862 | return target as T;
863 | }
864 |
865 | private void MoveConstraint(IConstraint constraint, GameObject targetObject)
866 | {
867 | IConstraint newConstraint = (IConstraint)targetObject.AddComponent(constraint.GetType());
868 | CopyValues((Component)newConstraint, (Component)constraint);
869 | for (var i = 0; i < constraint.sourceCount; i++)
870 | {
871 | var source = constraint.GetSource(i);
872 | var newSource = new ConstraintSource();
873 | newSource.sourceTransform = source.sourceTransform;
874 | newSource.weight = source.weight;
875 | newConstraint.AddSource(newSource);
876 | }
877 | DestroyImmediate((Component)constraint);
878 | }
879 |
880 | private void MoveVRChatConstraint(VRCConstraintBase constraint, GameObject targetObject)
881 | {
882 | VRCConstraintBase newConstraint = (VRCConstraintBase)targetObject.AddComponent(constraint.GetType());
883 | CopyValues((Component)newConstraint, (Component)constraint);
884 | DestroyImmediate((Component)constraint);
885 | }
886 |
887 | private GameObject InstallSystem(VRCAvatarDescriptor descriptor, AnimatorController mergedController,
888 | VRCExpressionParameters parameterObject)
889 | {
890 | GameObject rootObject = descriptor.gameObject;
891 | GameObject syncSystem = Instantiate(resourcePrefab, rootObject.transform);
892 | syncSystem.name = syncSystem.name.Replace("(Clone)", "");
893 | if (!rotationEnabled)
894 | {
895 | DestroyImmediate(syncSystem.transform.Find("Measure/Rotation").gameObject);
896 | DestroyImmediate(syncSystem.transform.Find("Set/Result").GetComponent());
897 | }
898 |
899 | foreach (string s in axis)
900 | {
901 | Transform sender = syncSystem.transform.Find($"Measure/Position/Sender{s}");
902 | float radius = Mathf.Pow(2, maxRadius);
903 | VRCPositionConstraint sendConstraint = sender.GetComponent();
904 | sendConstraint.PositionAtRest = new Vector3(0, 0, 0);
905 | VRCConstraintSource source0 = sendConstraint.Sources[0];
906 | source0.Weight = 1 - (3f / (radius));
907 | sendConstraint.Sources[0] = source0;
908 | VRCConstraintSource source1 = sendConstraint.Sources[1];
909 | source1.Weight = 3f / (radius);
910 | sendConstraint.Sources[1] = source1;
911 | }
912 |
913 | Transform mainTargetObject = syncSystem.transform.Find("Target");
914 | VRCParentConstraint mainTargetVRCParentConstraint = mainTargetObject.gameObject.AddComponent();
915 | mainTargetVRCParentConstraint.Locked = true;
916 | mainTargetVRCParentConstraint.IsActive = true;
917 | for (var i = 0; i < syncObjects.Length; i++)
918 | {
919 | GameObject targetSyncObject = syncObjects[i];
920 | Transform targetObject = new GameObject($"{targetSyncObject.name} Target").transform;
921 | targetObject.parent = targetSyncObject.transform.parent;
922 | targetObject.localPosition = targetSyncObject.transform.localPosition;
923 | targetObject.localRotation = targetSyncObject.transform.localRotation;
924 | targetObject.localScale = targetSyncObject.transform.localScale;
925 | mainTargetVRCParentConstraint.Sources.Add(new VRCConstraintSource(targetObject, 0f, Vector3.zero, Vector3.zero));
926 | string oldPath = GetDescriptorPath(targetSyncObject);
927 | targetSyncObject.transform.parent = syncSystem.transform;
928 | if (targetSyncObject.name == "Target") targetSyncObject.name = "User Target";
929 | string newPath = GetDescriptorPath(targetSyncObject);
930 | AnimationClip[] allClips = descriptor.baseAnimationLayers.Concat(descriptor.specialAnimationLayers)
931 | .Where(x => x.animatorController != null).SelectMany(x => x.animatorController.animationClips)
932 | .ToArray();
933 | RenameClipPaths(allClips, false, oldPath, newPath);
934 |
935 | // Unity Constraints
936 | var components = targetSyncObject.GetComponents();
937 | string targetPath = GetDescriptorPath(targetObject);
938 | for (var j = 0; j < components.Length; j++)
939 | {
940 | // Move constraint animations to the new path.
941 | AnimationClip[] targetClips = allClips.Where(x =>
942 | AnimationUtility.GetCurveBindings(x)
943 | .Any(y => y.type == components[j].GetType() && y.path == newPath)).ToArray();
944 | foreach (AnimationClip animationClip in targetClips)
945 | {
946 | EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
947 | for (var bi = 0; bi < curveBindings.Length; bi++)
948 | {
949 | var curveBinding = curveBindings[bi];
950 | AnimationCurve curve = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
951 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, null);
952 | if (curveBinding.type == components[j].GetType() && curveBinding.path == newPath)
953 | {
954 | curveBinding.path = targetPath;
955 | }
956 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, curve);
957 | }
958 | }
959 |
960 | MoveConstraint(components[j], targetObject.gameObject);
961 | }
962 |
963 | // VRChat Constraints
964 | var vrcComponents = targetSyncObject.GetComponents();
965 | string vrcTargetPath = GetDescriptorPath(targetObject);
966 | for (var j = 0; j < vrcComponents.Length; j++)
967 | {
968 | // Move constraint animations to the new path.
969 | AnimationClip[] targetClips = allClips.Where(x =>
970 | AnimationUtility.GetCurveBindings(x)
971 | .Any(y => y.type == vrcComponents[j].GetType() && y.path == newPath)).ToArray();
972 | foreach (AnimationClip animationClip in targetClips)
973 | {
974 | EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
975 | for (var bi = 0; bi < curveBindings.Length; bi++)
976 | {
977 | var curveBinding = curveBindings[bi];
978 | AnimationCurve curve = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
979 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, null);
980 | if (curveBinding.type == vrcComponents[j].GetType() && curveBinding.path == newPath)
981 | {
982 | curveBinding.path = vrcTargetPath;
983 | }
984 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, curve);
985 | }
986 | }
987 |
988 | MoveVRChatConstraint(vrcComponents[j], targetObject.gameObject);
989 | }
990 |
991 | GameObject damping = null;
992 | if (addDampeningConstraint)
993 | {
994 | damping = new GameObject($"{targetSyncObject.name} Damping Sync");
995 | damping.transform.parent = syncSystem.transform;
996 | VRCParentConstraint targetConstraint = targetSyncObject.AddComponent();
997 | targetConstraint.Locked = true;
998 | targetConstraint.IsActive = true;
999 | targetConstraint.Sources.Add(new VRCConstraintSource(damping.transform, 1f, Vector3.zero, Vector3.zero));
1000 | targetConstraint.Sources.Add(new VRCConstraintSource(targetSyncObject.transform, 0f, Vector3.zero, Vector3.zero));
1001 | }
1002 |
1003 | VRCParentConstraint containerConstraint = addDampeningConstraint ? damping.AddComponent() : targetSyncObject.AddComponent();
1004 | containerConstraint.Sources.Add((new VRCConstraintSource(targetObject, 1f, Vector3.zero, Vector3.zero)));
1005 | containerConstraint.Sources.Add((new VRCConstraintSource(syncSystem.transform.Find("Set/Result"), 0f, Vector3.zero, Vector3.zero)));
1006 | containerConstraint.Locked = true;
1007 | containerConstraint.IsActive = true;
1008 |
1009 | VRCScaleConstraint VRCScaleConstraint = targetSyncObject.gameObject.AddComponent();
1010 | VRCScaleConstraint.Sources.Add(new VRCConstraintSource(targetObject, 1f, Vector3.zero, Vector3.zero));
1011 | VRCScaleConstraint.Locked = true;
1012 | VRCScaleConstraint.IsActive = true;
1013 | }
1014 | Transform setTransform = syncSystem.transform.Find("Set");
1015 | Transform measureTransform = syncSystem.transform.Find("Measure");
1016 | float contactBugOffset = Mathf.Pow(2, maxRadius - 6); // Fixes a bug where proximity contacts near edges give 0, so we set this theoretical 0 to far away from spawn to reduce chances of this happening
1017 |
1018 |
1019 | Transform contactx = measureTransform.Find("Position/SenderX");
1020 | contactx.GetComponent().PositionOffset = new Vector3(contactBugOffset / Mathf.Pow(2, maxRadius) * 3, 0, 0);
1021 | Transform contacty = measureTransform.Find("Position/SenderY");
1022 | contacty.GetComponent().PositionOffset = new Vector3(0, contactBugOffset / Mathf.Pow(2, maxRadius) * 3, 0);
1023 | Transform contactz = measureTransform.Find("Position/SenderZ");
1024 | contactz.GetComponent().PositionOffset = new Vector3(0, 0, contactBugOffset / Mathf.Pow(2, maxRadius) * 3);
1025 |
1026 |
1027 | setTransform.localPosition = new Vector3(-contactBugOffset, -contactBugOffset, -contactBugOffset);
1028 | if (centeredOnAvatar || quickSync)
1029 | {
1030 | VRCPositionConstraint setConstraint = setTransform.gameObject.AddComponent();
1031 | VRCPositionConstraint measureConstraint = measureTransform.gameObject.AddComponent();
1032 | setConstraint.Sources.Add(new VRCConstraintSource(descriptor.transform, 1f, Vector3.zero, Vector3.zero));
1033 | measureConstraint.Sources.Add(new VRCConstraintSource(descriptor.transform, 1f, Vector3.zero, Vector3.zero));
1034 | setConstraint.PositionAtRest = Vector3.zero;
1035 | setConstraint.PositionOffset = new Vector3(-contactBugOffset, -contactBugOffset, -contactBugOffset);
1036 | setConstraint.Locked = true;
1037 | setConstraint.IsActive = true;
1038 | measureConstraint.PositionAtRest = Vector3.zero;
1039 | measureConstraint.PositionOffset = Vector3.zero;
1040 | measureConstraint.Locked = true;
1041 | measureConstraint.IsActive = true;
1042 | }
1043 |
1044 | VRCAvatarDescriptor.CustomAnimLayer[] layers = descriptor.baseAnimationLayers;
1045 | int fxLayerIndex = layers.ToList().FindIndex(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX);
1046 | VRCAvatarDescriptor.CustomAnimLayer fxLayer = layers[fxLayerIndex];
1047 | fxLayer.isDefault = false;
1048 | fxLayer.animatorController = mergedController;
1049 | layers[fxLayerIndex] = fxLayer;
1050 | descriptor.baseAnimationLayers = layers;
1051 |
1052 | descriptor.customExpressions = true;
1053 | descriptor.expressionParameters = parameterObject;
1054 |
1055 | Selection.activeObject = descriptor.gameObject;
1056 | return syncSystem;
1057 | }
1058 |
1059 | private AnimatorControllerLayer SetupSyncLayer(int syncSteps, int positionBits, int rotationBits, int objectCount,
1060 | int objectParameterCount, bool[][] objectPerms, int syncStepParameterCount, bool[][] syncStepPerms)
1061 | {
1062 | AnimatorStateMachine syncMachine = GenerateStateMachine("CustomObjectSync/Sync", new Vector3(-80, 0, 0), new Vector3(-80, 200 , 0), new Vector3(-80, 100, 0));
1063 | AnimatorControllerLayer syncLayer = GenerateLayer("CustomObjectSync/Sync", syncMachine);
1064 |
1065 | AnimationClip bufferWaitInit = GenerateClip($"BufferWait{(int)(syncSteps*1.5f)}");
1066 | AddCurve(bufferWaitInit, "Custom Object Sync/Measure", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, ((Math.Max(positionBits, rotationBits)*1.5f))/60f, 0));
1067 | AddCurve(bufferWaitInit, "Custom Object Sync/Set", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, ((Math.Max(positionBits, rotationBits)*1.5f))/60f, 0));
1068 |
1069 | AnimationClip bufferWaitSync = GenerateClip($"BufferWaitSync");
1070 | AddCurve(bufferWaitSync, "Custom Object Sync/Measure", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 12/60f, 0));
1071 | AddCurve(bufferWaitSync, "Custom Object Sync/Set", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 12/60f, 0));
1072 |
1073 | AnimationClip enableWorldConstraint = GenerateClip($"EnableWorldConstraint");
1074 | AddCurve(enableWorldConstraint, "Custom Object Sync/Measure", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 12/60f, 1));
1075 | AddCurve(enableWorldConstraint, "Custom Object Sync/Set", typeof(VRCPositionConstraint), "m_Enabled", AnimationCurve.Constant(0, 12/60f, 1));
1076 |
1077 | #region SyncStates
1078 | ChildAnimatorState initState = GenerateChildState(new Vector3(-100, -150, 0), GenerateState("SyncInit", motion: enableWorldConstraint));
1079 | List localStates = new List();
1080 | List remoteStates = new List();
1081 | if (quickSync)
1082 | {
1083 | string[] syncParameterNames = axis.Select(x => $"Position{x}").Concat(axis.Select(x => $"PositionSign{x}")).ToArray();
1084 | if (rotationEnabled)
1085 | {
1086 | syncParameterNames = syncParameterNames.Concat(axis.Select(x => $"Rotation{x}")).ToArray();
1087 | }
1088 | for (int o = 0; o < objectCount; o++)
1089 | {
1090 | localStates.Add(GenerateChildState(new Vector3(-500,-(objectCount) * 25 + ((o + 1) * 50), 0), GenerateState($"SyncLocal{o}", motion: bufferWaitSync)));
1091 | localStates.Add(GenerateChildState(new Vector3(-800, -(objectCount) * 25 + ((o + 1) * 50), 0), GenerateState($"SyncLocal{o}Buffer", motion: bufferWaitSync)));
1092 | localStates.Last().state.behaviours = new[]
1093 | {
1094 | GenerateParameterDriver(Enumerable.Range(0, objectParameterCount).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Sync/Object{x}", value: objectPerms[(o + 1) % objectCount][x] ? 1 : 0)).ToArray()),
1095 | GenerateParameterDriver(syncParameterNames.Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/{x}", name: $"CustomObjectSync/Sync/{x}")).ToArray()),
1096 | GenerateParameterDriver(Enumerable.Range(0, objectParameterCount).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/LocalReadBit{x}", value: objectPerms[(o + 1) % objectCount][x] ? 1f : 0f)).ToArray())
1097 | };
1098 |
1099 | remoteStates.Add(GenerateChildState(new Vector3(300, -(objectCount) * 25 + ((o + 1) * 50), 0), GenerateState($"SyncRemote{o}")));
1100 | remoteStates.Last().state.behaviours = new[]
1101 | {
1102 | GenerateParameterDriver(syncParameterNames.Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Sync/{x}", name: $"CustomObjectSync/{x}")).ToArray())
1103 | };
1104 | }
1105 | }
1106 | else
1107 | {
1108 | string[] syncParameterNames = axis
1109 | .SelectMany(n => Enumerable.Range(0, positionBits).Select(i => $"CustomObjectSync/Bits/Copy/Position{n}{i}"))
1110 | .Concat(axis.SelectMany(n =>
1111 | Enumerable.Range(0, rotationBits).Select(i => $"CustomObjectSync/Bits/Copy/Rotation{n}{i}"))).ToArray();
1112 | string[][] localSyncParameterNames = Enumerable.Range(0, objectCount).Select(o => axis
1113 | .SelectMany(n => Enumerable.Range(0, positionBits).Select(i => $"CustomObjectSync/Bits/Position{n}{i}/{o}"))
1114 | .Concat(axis.SelectMany(n =>
1115 | Enumerable.Range(0, rotationBits).Select(i => $"CustomObjectSync/Bits/Rotation{n}{i}/{o}"))).ToArray()).ToArray();
1116 |
1117 | int stepToStartSync = Mathf.CeilToInt(Math.Max(positionBits, rotationBits)*1.5f / 12f);
1118 | bool shouldDelayFirst = (stepToStartSync > syncSteps);
1119 |
1120 | if (shouldDelayFirst)
1121 | {
1122 | stepToStartSync = syncSteps;
1123 | }
1124 |
1125 |
1126 | int totalSyncSteps = objectCount * syncSteps;
1127 |
1128 | for (int i = 0; i < totalSyncSteps; i++)
1129 | {
1130 | int o = i / syncSteps;
1131 | int s = i % syncSteps;
1132 | localStates.Add(GenerateChildState(new Vector3(-500,-(totalSyncSteps) * 25 + ((i + 1) * 50), 0), GenerateState($"SyncLocal{i}", motion: bufferWaitSync)));
1133 | if (shouldDelayFirst && i % syncSteps == 0)
1134 | {
1135 | localStates.Last().state.motion = bufferWaitInit;
1136 | }
1137 |
1138 | if (i % syncSteps == syncSteps - 1)
1139 | {
1140 | // When we begin sending copy out values so we have them ready to send
1141 | localStates.Last().state.behaviours = localStates.Last().state.behaviours
1142 | .Append(GenerateParameterDriver(Enumerable.Range(0, syncParameterNames.Length).Select(x => GenerateParameter(ChangeType.Copy, localSyncParameterNames[(o + 1) % objectCount][x], syncParameterNames[x])).ToArray())).ToArray();
1143 | }
1144 |
1145 | if (syncSteps - s == stepToStartSync)
1146 | {
1147 | localStates.Last().state.behaviours = localStates.Last().state.behaviours
1148 | .Append(GenerateParameterDriver(
1149 | Enumerable.Range(0, objectParameterCount).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/LocalReadBit{x}", value: objectPerms[(o + 1) % objectCount][x] ? 1f : 0f)).Append(
1150 | GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/StartRead/{(o + 1) % objectCount}", value: 1)).ToArray()))
1151 | .ToArray();
1152 | }
1153 |
1154 |
1155 | localStates.Add(GenerateChildState(new Vector3(-800, -(totalSyncSteps) * 25 + ((i + 1) * 50), 0), GenerateState($"SyncLocal{i}Buffer", motion: bufferWaitSync)));
1156 | localStates.Last().state.behaviours = new[]
1157 | {
1158 | GenerateParameterDriver(Enumerable.Range(0, syncStepParameterCount).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Sync/Step{x}", value: syncStepPerms[(i + 1) % (syncSteps)][x] ? 1 : 0)).ToArray()),
1159 | GenerateParameterDriver(Enumerable.Range(0, objectParameterCount).Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Sync/Object{x}", value: objectPerms[((i + 1) % (totalSyncSteps))/ syncSteps][x] ? 1 : 0)).ToArray()),
1160 | GenerateParameterDriver(
1161 | Enumerable
1162 | .Range(((s + 1) % syncSteps) * bitCount, Math.Min((((s + 1) % syncSteps) + 1) * bitCount, GetMaxBitCount()) - ((((s + 1) % syncSteps)) * bitCount))
1163 | .Select(x => GenerateParameter(ChangeType.Copy, source: syncParameterNames[x],
1164 | name: $"CustomObjectSync/Sync/Data{x % bitCount}"))
1165 | .ToArray())
1166 | };
1167 | }
1168 |
1169 | for (int i = 0; i < totalSyncSteps; i++)
1170 | {
1171 | int o = i / syncSteps;
1172 | int s = i % syncSteps;
1173 | remoteStates.Add(GenerateChildState(new Vector3(300, -(totalSyncSteps) * 25 + ((i + 1) * 50), 0), GenerateState($"SyncRemote{i}", motion: bufferWaitSync)));
1174 | remoteStates.Last().state.behaviours = new[]
1175 | {
1176 | GenerateParameterDriver(
1177 | Enumerable
1178 | .Range(s * bitCount, Math.Min((s + 1) * bitCount, GetMaxBitCount()) - (s * bitCount))
1179 | .Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Sync/Data{x % bitCount}", name: syncParameterNames[x]))
1180 | .ToArray())
1181 | };
1182 | if (s == syncSteps - 1)
1183 | {
1184 | remoteStates.Last().state.behaviours =
1185 | remoteStates.Last().state.behaviours.Append(GenerateParameterDriver(new [] {GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/StartWrite/{o}", value: 1)})).ToArray();
1186 | remoteStates.Last().state.behaviours = remoteStates.Last().state.behaviours
1187 | .Append(GenerateParameterDriver(Enumerable.Range(0, syncParameterNames.Length).Select(x => GenerateParameter(ChangeType.Copy, syncParameterNames[x], localSyncParameterNames[o][x])).ToArray())).ToArray();
1188 | }
1189 | }
1190 | }
1191 | #endregion
1192 |
1193 | #region SyncTransitions
1194 |
1195 | List syncAnyStateTransitions = new List();
1196 |
1197 | syncAnyStateTransitions.Add(GenerateTransition("", conditions: new [] {GenerateCondition(AnimatorConditionMode.IfNot, "CustomObjectSync/Enabled", 0)}, destinationState: initState.state));
1198 |
1199 | if (quickSync)
1200 | {
1201 | for (int i = 0; i < localStates.Count/2; i++)
1202 | {
1203 | syncAnyStateTransitions.Add(GenerateTransition("", conditions:
1204 | Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[i][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Object{x}", threshold: 0f))
1205 | .Append(GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0))
1206 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0)).ToArray(), destinationState: localStates[i * 2].state));
1207 |
1208 | localStates[i * 2].state.transitions = new[]
1209 | {
1210 | GenerateTransition("", destinationState: localStates[(i*2)+1].state, hasExitTime: true, exitTime: 1)
1211 | };
1212 | }
1213 |
1214 | for (int i = 0; i < remoteStates.Count; i++)
1215 | {
1216 | int o = i;
1217 | syncAnyStateTransitions.Add(GenerateTransition("", canTransitionToSelf: true, conditions:
1218 | Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[o][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Object{x}", threshold: 0f))
1219 | .Append(GenerateCondition(AnimatorConditionMode.IfNot, "IsLocal", 0))
1220 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0)).ToArray(), destinationState: remoteStates[i].state));
1221 | }
1222 | }
1223 | else
1224 | {
1225 | for (int i = 0; i < localStates.Count/2; i++)
1226 | {
1227 | int o = i / syncSteps;
1228 | syncAnyStateTransitions.Add(GenerateTransition("", conditions: Enumerable.Range(0, syncStepParameterCount)
1229 | .Select(x => GenerateCondition(syncStepPerms[(i) % (syncSteps)][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Step{x}", 0))
1230 | .Concat(Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[o][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Object{x}", threshold: 0f)))
1231 | .Append(GenerateCondition(AnimatorConditionMode.If, "IsLocal", 0))
1232 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0)).ToArray(), destinationState: localStates[i * 2].state));
1233 |
1234 | localStates[i * 2].state.transitions = new[]
1235 | {
1236 | GenerateTransition("", destinationState: localStates[(i*2)+1].state, hasExitTime: true, exitTime: 1)
1237 | };
1238 | }
1239 |
1240 | for (int i = 0; i < remoteStates.Count; i++)
1241 | {
1242 | int o = i / syncSteps;
1243 | syncAnyStateTransitions.Add(GenerateTransition("", canTransitionToSelf: true, conditions: Enumerable.Range(0, syncStepParameterCount)
1244 | .Select(x => GenerateCondition(syncStepPerms[(i) % (syncSteps)][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Step{x}", 0))
1245 | .Concat(Enumerable.Range(0, objectParameterCount).Select(x => GenerateCondition(objectPerms[o][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Object{x}", threshold: 0f)))
1246 | .Append(GenerateCondition(AnimatorConditionMode.IfNot, "IsLocal", 0))
1247 | .Append(GenerateCondition(AnimatorConditionMode.If, "CustomObjectSync/Enabled", 0)).ToArray(), destinationState: remoteStates[i].state));
1248 | }
1249 | }
1250 |
1251 | syncMachine.anyStateTransitions = syncAnyStateTransitions.ToArray();
1252 | #endregion
1253 |
1254 | syncMachine.states = localStates.Concat(remoteStates).Concat(new [] { initState }).ToArray();
1255 | syncMachine.defaultState = initState.state;
1256 | return syncLayer;
1257 | }
1258 |
1259 | private void SetupSyncLayerParameters(int syncSteps, int objectCount, int objectParameterCount,
1260 | int syncStepParameterCount, bool[][] syncStepPerms, AnimatorController mergedController)
1261 | {
1262 | List syncParameters = new List();
1263 | for (int i = 0; i < objectParameterCount; i++)
1264 | {
1265 | syncParameters.Add(GenerateBoolParameter( $"CustomObjectSync/Sync/Object{i}", false));
1266 | }
1267 | if (quickSync)
1268 | {
1269 | syncParameters = syncParameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/Sync/Position{x}"))).ToList();
1270 | syncParameters = syncParameters.Concat(axis.Select(x => GenerateBoolParameter($"CustomObjectSync/Sync/PositionSign{x}"))).ToList();
1271 | if (rotationEnabled)
1272 | {
1273 | syncParameters = syncParameters.Concat(axis.Select(x => GenerateFloatParameter($"CustomObjectSync/Sync/Rotation{x}"))).ToList();
1274 | }
1275 | }
1276 | else
1277 | {
1278 | for (int i = 0; i < bitCount; i++)
1279 | {
1280 | syncParameters.Add(GenerateBoolParameter( $"CustomObjectSync/Sync/Data{i}", false));
1281 | }
1282 | for (int i = 0; i < syncStepParameterCount; i++)
1283 | {
1284 | syncParameters.Add(GenerateBoolParameter( $"CustomObjectSync/Sync/Step{i}", syncStepPerms[syncSteps-1][i]));
1285 | }
1286 |
1287 | for (int i = 0; i < objectParameterCount; i++)
1288 | {
1289 | syncParameters.Add(GenerateBoolParameter( $"CustomObjectSync/Object{i}", false));
1290 | for (int o = 0; o < objectCount; o++)
1291 | {
1292 | syncParameters.Add(GenerateBoolParameter( $"CustomObjectSync/Temp/Object{o}-{i}", false));
1293 | }
1294 | }
1295 | }
1296 | mergedController.parameters = mergedController.parameters.Concat(syncParameters).ToArray();
1297 | }
1298 |
1299 | private void AddBitConversionParameters(int positionBits, List parameters, int objectCount, int rotationBits)
1300 | {
1301 | for (int p = 0; p < axis.Length; p++)
1302 | {
1303 | for (int b = 0; b < positionBits; b++)
1304 | {
1305 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/Bits/Copy/Position{axis[p]}{b}"));
1306 | }
1307 | }
1308 |
1309 | for (int o = 0; o < objectCount; o++)
1310 | {
1311 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/ReadObject/{o}"));
1312 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/StartWrite/{o}"));
1313 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/StartRead/{o}"));
1314 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/ReadInProgress/{o}"));
1315 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/WriteInProgress/{o}"));
1316 | for (int p = 0; p < axis.Length; p++)
1317 | {
1318 | parameters.Add(GenerateFloatParameter($"CustomObjectSync/Temp/Position{axis[p]}/{o}"));
1319 | for (int b = 0; b < positionBits; b++)
1320 | {
1321 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/Bits/Position{axis[p]}{b}/{o}"));
1322 | }
1323 | }
1324 | }
1325 |
1326 | if (rotationEnabled)
1327 | {
1328 | for (int p = 0; p < axis.Length; p++)
1329 | {
1330 | for (int b = 0; b < rotationBits; b++)
1331 | {
1332 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/Bits/Copy/Rotation{axis[p]}{b}"));
1333 | }
1334 | }
1335 |
1336 | for (int o = 0; o < objectCount; o++)
1337 | {
1338 | for (int p = 0; p < axis.Length; p++)
1339 | {
1340 | parameters.Add(GenerateFloatParameter($"CustomObjectSync/Temp/Rotation{axis[p]}/{o}"));
1341 | for (int b = 0; b < rotationBits; b++)
1342 | {
1343 | parameters.Add(GenerateBoolParameter($"CustomObjectSync/Bits/Rotation{axis[p]}{b}/{o}"));
1344 | }
1345 | }
1346 | }
1347 | }
1348 | }
1349 |
1350 | private List GenerateBitConversionLayers(int objectCount, AnimationClip buffer, int positionBits,
1351 | int objectParameterCount, int rotationBits)
1352 | {
1353 | List bitLayers = new List();
1354 | for (int o = 0; o < objectCount; o++)
1355 | {
1356 | AnimatorStateMachine positionMachine = GenerateStateMachine($"CustomObjectSync/Position Bit Convert{o}", new Vector3(-80, 0, 0), new Vector3(-80, 200 , 0), new Vector3(-80, 100, 0));
1357 | AnimatorControllerLayer positionLayer = GenerateLayer($"CustomObjectSync/Position Bit Convert{o}", positionMachine);
1358 | ChildAnimatorState initialState = GenerateChildState(new Vector3(-100, 400, 0), GenerateState("Initial", motion: buffer));
1359 |
1360 | SetupAnimationControllerCopy("Position", o, buffer, initialState, positionMachine, positionBits, objectParameterCount, true, positionBits > rotationBits);
1361 | SetupAnimationControllerCopy("Position", o, buffer, initialState, positionMachine, positionBits, objectParameterCount, false, positionBits > rotationBits);
1362 | positionMachine.states = new[] { initialState }.Concat(positionMachine.states).ToArray();
1363 | positionMachine.defaultState = initialState.state;
1364 | bitLayers.Add(positionLayer);
1365 |
1366 | if (rotationEnabled)
1367 | {
1368 | AnimatorStateMachine rotationMachine = GenerateStateMachine($"CustomObjectSync/Rotation Bit Convert{o}", new Vector3(-80, 0, 0), new Vector3(-80, 200 , 0), new Vector3(-80, 100, 0));
1369 | AnimatorControllerLayer rotationLayer = GenerateLayer($"CustomObjectSync/Rotation Bit Convert{o}", rotationMachine);
1370 | ChildAnimatorState initialRotationState = GenerateChildState(new Vector3(-100, 400, 0), GenerateState("Initial", motion: buffer));
1371 |
1372 | SetupAnimationControllerCopy("Rotation", o, buffer, initialRotationState, rotationMachine, rotationBits, objectParameterCount, true, positionBits <= rotationBits);
1373 | SetupAnimationControllerCopy("Rotation", o, buffer, initialRotationState, rotationMachine, rotationBits, objectParameterCount, false, positionBits <= rotationBits);
1374 | rotationMachine.states = new[] { initialRotationState }.Concat(rotationMachine.states).ToArray();
1375 | rotationMachine.defaultState = initialRotationState.state;
1376 | bitLayers.Add(rotationLayer);
1377 | }
1378 | }
1379 |
1380 | return bitLayers;
1381 | }
1382 |
1383 | private void SetupAnimationControllerCopy(string name, int o, AnimationClip buffer, ChildAnimatorState initialState,
1384 | AnimatorStateMachine machine, int bits, int objectBits, bool read, bool copyBoth)
1385 | {
1386 | bool[][] permutations = GeneratePermutations(3);
1387 | bool[][] objectPermutations = GeneratePermutations(objectBits);
1388 |
1389 | int multiplier = read ? -1 : 1;
1390 | string mode = read ? "Read" : "Write";
1391 |
1392 | #region states
1393 | ChildAnimatorState startState = GenerateChildState(new Vector3((-100) + multiplier * 300, 200, 0), GenerateState($"Start{mode}", motion: buffer));
1394 | ChildAnimatorState endState = GenerateChildState(new Vector3((-100) + multiplier * 300 * (bits + 2) , 200, 0), GenerateState($"End{mode}", motion: buffer));
1395 |
1396 | startState.state.behaviours = new[]
1397 | {
1398 | read
1399 | ? GenerateParameterDriver(axis.Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/{name}{x}", name: $"CustomObjectSync/Temp/{name}{x}/{o}")).ToArray())
1400 | : GenerateParameterDriver(axis.Select(x => GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Temp/{name}{x}/{o}", value: 0)).ToArray())
1401 | };
1402 |
1403 | if (copyBoth)
1404 | {
1405 | endState.state.behaviours = new[]
1406 | {
1407 | GenerateParameterDriver(new[]
1408 | {
1409 | GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Start{mode}/{o}", value: 0) ,
1410 | GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/{mode}InProgress/{o}", value: 0)
1411 | })
1412 | };
1413 | startState.state.behaviours = startState.state.behaviours.Append(GenerateParameterDriver(new [] {GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/{mode}InProgress/{o}", value: 1)})).ToArray();
1414 | }
1415 |
1416 |
1417 | if (!read && copyBoth)
1418 | {
1419 | endState.state.behaviours = endState.state.behaviours.Append(GenerateParameterDriver(axis.Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Temp/Position{x}/{o}", name: $"CustomObjectSync/Position{x}")).ToArray())).ToArray();
1420 | endState.state.behaviours = endState.state.behaviours.Append(GenerateParameterDriver(axis.Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Temp/Rotation{x}/{o}", name: $"CustomObjectSync/Rotation{x}")).ToArray())).ToArray();
1421 | startState.state.behaviours = startState.state.behaviours.Append(GenerateParameterDriver(Enumerable.Range(0 ,objectBits).Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Sync/Object{x}", name: $"CustomObjectSync/Temp/Object{o}-{x}")).ToArray())).ToArray();
1422 | endState.state.behaviours = endState.state.behaviours.Append(GenerateParameterDriver(Enumerable.Range(0 ,objectBits).Select(x => GenerateParameter(ChangeType.Copy, source: $"CustomObjectSync/Temp/Object{o}-{x}", name: $"CustomObjectSync/Object{x}")).ToArray())).ToArray();
1423 | }
1424 |
1425 | List> bitStates = new List>();
1426 | for (int b = 0; b < bits; b++)
1427 | {
1428 | List states = new List();
1429 | bitStates.Add(states);
1430 | for (int s = 0; s < 8; s++)
1431 | {
1432 | bool[] perm = permutations[s];
1433 | string permString = perm.Aggregate("", (s1, b1) => s1 += (b1 ? "1" : "0"));
1434 | AnimatorState state = GenerateState($"Bit{mode}-{b}-{permString}", motion: buffer);
1435 |
1436 | List parameterDriverParams = new List();
1437 | for (int p = 0; p < perm.Length; p++)
1438 | {
1439 | if (read)
1440 | {
1441 | if (perm[p])
1442 | {
1443 | parameterDriverParams.Add(GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Bits/{name}{axis[p]}{b}/{o}", value: 1));
1444 | parameterDriverParams.Add(GenerateParameter(ChangeType.Add, name: $"CustomObjectSync/Temp/{name}{axis[p]}/{o}", value: -1 * Mathf.Pow(0.5f, b + 1)));
1445 | }
1446 | else
1447 | {
1448 | parameterDriverParams.Add(GenerateParameter(ChangeType.Set, name: $"CustomObjectSync/Bits/{name}{axis[p]}{b}/{o}", value: 0));
1449 | }
1450 | }
1451 | else
1452 | {
1453 | if (perm[p])
1454 | {
1455 | parameterDriverParams.Add(GenerateParameter(ChangeType.Add, name: $"CustomObjectSync/Temp/{name}{axis[p]}/{o}", value: 1 * Mathf.Pow(0.5f, b + 1)));
1456 | }
1457 | }
1458 | }
1459 | state.behaviours = new StateMachineBehaviour[]
1460 | {
1461 | GenerateParameterDriver(parameterDriverParams.ToArray())
1462 | };
1463 | states.Add(GenerateChildState(new Vector3((-100) + multiplier * (b + 2) * 300, s * 50 ), state));
1464 | }
1465 | }
1466 |
1467 | #endregion
1468 |
1469 | #region transitions
1470 |
1471 | initialState.state.transitions = initialState.state.transitions
1472 | .Append(
1473 | GenerateTransition("", destinationState: startState.state, conditions:
1474 | Enumerable.Range(0, objectBits).Select(x => GenerateCondition(objectPermutations[o][x] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, $"CustomObjectSync/Sync/Object{x}", threshold: 0f))
1475 | .Append(GenerateCondition(AnimatorConditionMode.If, $"CustomObjectSync/Start{mode}/{o}", 0))
1476 | .Append(GenerateCondition( read ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, "IsLocal", 0)).ToArray())
1477 | ).ToArray();
1478 | startState.state.transitions = bitStates[0].Select((x, index) => GenerateBitTransition(permutations[index], 0, o, name, x.state, read)).ToArray();
1479 |
1480 | for (var i = 0; i < bitStates.Count; i++)
1481 | {
1482 | if (i == bitStates.Count - 1)
1483 | {
1484 | for (var s = 0; s < bitStates[i].Count; s++)
1485 | {
1486 | bitStates[i][s].state.transitions = new[]
1487 | {
1488 | GenerateTransition("", destinationState: endState.state, hasExitTime: true, exitTime: 1)
1489 | };
1490 | }
1491 | continue;
1492 | }
1493 |
1494 | for (int s1 = 0; s1 < bitStates[i].Count; s1++)
1495 | {
1496 | AnimatorStateTransition[] transitions = new AnimatorStateTransition[bitStates[i + 1].Count];
1497 | for (int s2 = 0; s2 < bitStates[i+1].Count; s2++)
1498 | {
1499 | transitions[s2] = GenerateBitTransition(permutations[s2], i + 1, o, name, bitStates[i + 1][s2].state, read);
1500 | }
1501 | bitStates[i][s1].state.transitions = transitions;
1502 | }
1503 | }
1504 |
1505 | endState.state.transitions = new[] { GenerateTransition("", destinationState: initialState.state, conditions: new []{GenerateCondition(AnimatorConditionMode.IfNot, $"CustomObjectSync/{mode}InProgress/{o}", 0f)}) };
1506 |
1507 | #endregion
1508 |
1509 | machine.states = machine.states.Concat(new []{ startState, endState }).Concat(bitStates.SelectMany(x => x)).ToArray();
1510 | }
1511 |
1512 | public void Remove()
1513 | {
1514 | VRCAvatarDescriptor descriptor = syncObjects.Select(x => x.GetComponentInParent()).FirstOrDefault();
1515 |
1516 | if (descriptor == null) return;
1517 |
1518 | Stack menus = new Stack(new [] { descriptor.expressionsMenu });
1519 | while (menus.Count > 0)
1520 | {
1521 | VRCExpressionsMenu menu = menus.Pop();
1522 | if (menu == null || menu.controls == null) continue;
1523 | if (menu.controls.Any(x =>
1524 | x.type == VRCExpressionsMenu.Control.ControlType.Toggle &&
1525 | x.parameter.name == "CustomObjectSync/Enabled"))
1526 | {
1527 | menu.controls = menu.controls.Where(x => x.parameter.name != "CustomObjectSync/Enabled" && x.parameter.name != "CustomObjectSync/LocalDebugView").ToList();
1528 | EditorUtility.SetDirty(menu);
1529 | }
1530 | menu.controls
1531 | .Where(x => x.type == VRCExpressionsMenu.Control.ControlType.SubMenu && x.subMenu != null)
1532 | .ToList().ForEach(x => menus.Push(x.subMenu));
1533 | }
1534 |
1535 | if (descriptor.expressionParameters != null && descriptor.expressionParameters.parameters != null &&
1536 | descriptor.expressionParameters.parameters.Any(x => x.name.Contains("CustomObjectSync")))
1537 | {
1538 | descriptor.expressionParameters.parameters = descriptor.expressionParameters.parameters
1539 | .Where(x => !x.name.Contains("CustomObjectSync/")).ToArray();
1540 | EditorUtility.SetDirty(descriptor.expressionParameters);
1541 | }
1542 |
1543 | if (descriptor != null &&
1544 | descriptor.baseAnimationLayers.FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX)
1545 | .animatorController != null &&
1546 | ((AnimatorController)descriptor.baseAnimationLayers
1547 | .FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController).layers
1548 | .Any(x => x.name.Contains("CustomObjectSync")))
1549 | {
1550 | AnimatorController controller = ((AnimatorController)descriptor.baseAnimationLayers
1551 | .FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController);
1552 | AnimatorControllerLayer[] layersToDelete = controller.layers.Where(x => x.name.StartsWith("CustomObjectSync/")).ToArray();
1553 | List assets = new List();
1554 |
1555 | void AddToAssetsListStateMachineRecursive(List usedAssets, AnimatorStateMachine sm) {
1556 | usedAssets.Add(sm);
1557 | foreach (var behaviour in sm.behaviours) usedAssets.Add(behaviour);
1558 | foreach (var transition in sm.anyStateTransitions) usedAssets.Add(transition);
1559 | foreach (var transition in sm.entryTransitions) usedAssets.Add(transition);
1560 | foreach (var state in sm.states) {
1561 | usedAssets.Add(state.state);
1562 | foreach (var behaviour in state.state.behaviours) usedAssets.Add(behaviour);
1563 | foreach (var transition in state.state.transitions) usedAssets.Add(transition);
1564 | if (state.state.motion is BlendTree) {
1565 | usedAssets.Add(state.state.motion);
1566 | var bt = state.state.motion as BlendTree;
1567 | var stack = new Stack();
1568 | stack.Push(bt);
1569 | while (stack.Count > 0) {
1570 | var current = stack.Pop();
1571 | foreach (var child in current.children) {
1572 | if (child.motion is BlendTree)
1573 | {
1574 | usedAssets.Add(child.motion);
1575 | stack.Push(child.motion as BlendTree);
1576 | }
1577 | }
1578 | }
1579 | }
1580 | }
1581 | foreach (var state_machine in sm.stateMachines) AddToAssetsListStateMachineRecursive(assets, state_machine.stateMachine);
1582 | }
1583 |
1584 | foreach (var animatorControllerLayer in layersToDelete)
1585 | {
1586 | AddToAssetsListStateMachineRecursive(assets, animatorControllerLayer.stateMachine);
1587 | }
1588 | foreach (Object usedAsset in assets)
1589 | {
1590 | AssetDatabase.RemoveObjectFromAsset(usedAsset);
1591 | }
1592 |
1593 | controller.layers = controller.layers.Where(x => !x.name.StartsWith("CustomObjectSync/")).ToArray();
1594 | controller.parameters = controller.parameters.Where(x => !x.name.Contains("CustomObjectSync/")).ToArray();
1595 |
1596 | if (controller.layers.Count() < 1)
1597 | {
1598 | AnimatorControllerLayer baseLayer = new AnimatorControllerLayer
1599 | {
1600 | name = "Base Layer",
1601 | defaultWeight = 1f,
1602 | stateMachine = new AnimatorStateMachine()
1603 | };
1604 | AssetDatabase.AddObjectToAsset(baseLayer.stateMachine, controller);
1605 |
1606 | controller.AddLayer(baseLayer);
1607 | }
1608 | }
1609 |
1610 | if (descriptor.transform.Find("Custom Object Sync"))
1611 | {
1612 | void CantFindTarget(Transform userObject)
1613 | {
1614 | // Can't figure out the target, so can't move back to the old path. Just move the object to descriptor.
1615 | string oldPath = AnimationUtility.CalculateTransformPath(userObject.transform, descriptor.transform);
1616 | userObject.parent = descriptor.transform;
1617 | string newPath = AnimationUtility.CalculateTransformPath(userObject.transform, descriptor.transform);
1618 |
1619 | AnimationClip[] allClips = descriptor.baseAnimationLayers.Concat(descriptor.specialAnimationLayers)
1620 | .Where(x => x.animatorController != null).SelectMany(x => x.animatorController.animationClips)
1621 | .ToArray();
1622 | RenameClipPaths(allClips, false, oldPath, newPath);
1623 |
1624 |
1625 | if (userObject.GetComponent() != null)
1626 | {
1627 | DestroyImmediate(userObject.GetComponent());
1628 | }
1629 | }
1630 |
1631 | Transform prefab = descriptor.transform.Find("Custom Object Sync");
1632 | Transform[] userObjects = Enumerable.Range(0, prefab.childCount)
1633 | .Select(x => prefab.GetChild(x)).Where(x => x.name != "Set" && x.name != "Measure" && x.name != "Target" && !x.name.Contains(" Damping Sync")).ToArray();
1634 | foreach (Transform userObject in userObjects)
1635 | {
1636 | if(userObject == null) continue;
1637 | VRCParentConstraint targetConstraint = userObject.GetComponent();
1638 | if (targetConstraint == null)
1639 | {
1640 | CantFindTarget(userObject);
1641 | continue;
1642 | }
1643 | Transform dampingObj = targetConstraint.Sources[0].SourceTransform;
1644 | if (dampingObj == null)
1645 | {
1646 | CantFindTarget(userObject);
1647 | continue;
1648 | };
1649 |
1650 | VRCParentConstraint VRCParentConstraint = dampingObj.gameObject.GetComponent();
1651 | if (VRCParentConstraint != null && VRCParentConstraint.Sources.Count == 2 && VRCParentConstraint.Sources[1].SourceTransform != null &&
1652 | VRCParentConstraint.Sources[1].SourceTransform == prefab.transform.Find("Set/Result"))
1653 | {
1654 | targetConstraint = VRCParentConstraint;
1655 | }
1656 |
1657 | Transform target = Enumerable.Range(0, targetConstraint.Sources.Count)
1658 | .Select(x => targetConstraint.Sources[x]).Where(x => x.SourceTransform != null && x.SourceTransform.name.EndsWith("Target"))
1659 | .Select(x => x.SourceTransform).FirstOrDefault();
1660 |
1661 | if (target == null)
1662 | {
1663 | CantFindTarget(userObject);
1664 | continue;
1665 | }
1666 |
1667 | string oldPath = GetDescriptorPath(userObject);
1668 | if (GetDescriptorPath(target).StartsWith(AnimationUtility.CalculateTransformPath(prefab, descriptor.transform)))
1669 | {
1670 | target.parent = prefab.parent;// Make sure if the user put the target under the prefab, it doesnt get deleted.
1671 | }
1672 | userObject.parent = target.parent.transform;
1673 | string newPath = GetDescriptorPath(userObject);
1674 |
1675 | AnimationClip[] allClips = descriptor.baseAnimationLayers.Concat(descriptor.specialAnimationLayers)
1676 | .Where(x => x.animatorController != null).SelectMany(x => x.animatorController.animationClips)
1677 | .ToArray();
1678 | RenameClipPaths(allClips, false, oldPath, newPath);
1679 |
1680 | if (userObject.GetComponent() != null)
1681 | {
1682 | DestroyImmediate(userObject.GetComponent());
1683 | }
1684 | if (userObject.GetComponent() != null)
1685 | {
1686 | DestroyImmediate(userObject.GetComponent());
1687 | }
1688 |
1689 | // Unity Constraints
1690 | var components = target.GetComponents();
1691 | string targetPath = GetDescriptorPath(target);
1692 | for (var j = 0; j < components.Length; j++)
1693 | {
1694 | // Move constraint animations to the new path.
1695 | AnimationClip[] targetClips = allClips.Where(x =>
1696 | AnimationUtility.GetCurveBindings(x)
1697 | .Any(y => y.type == components[j].GetType() && y.path == targetPath)).ToArray();
1698 | foreach (AnimationClip animationClip in targetClips)
1699 | {
1700 | EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
1701 | for (var bi = 0; bi < curveBindings.Length; bi++)
1702 | {
1703 | var curveBinding = curveBindings[bi];
1704 | AnimationCurve curve = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
1705 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, null);
1706 | if (curveBinding.type == components[j].GetType() && curveBinding.path == targetPath)
1707 | {
1708 | curveBinding.path = newPath;
1709 | }
1710 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, curve);
1711 | }
1712 | }
1713 | MoveConstraint(components[j], userObject.gameObject);
1714 | }
1715 |
1716 | // VRChat Constraints
1717 | var vrcComponents = target.GetComponents();
1718 | string vrcTargetPath = GetDescriptorPath(target);
1719 | for (var j = 0; j < vrcComponents.Length; j++)
1720 | {
1721 | // Move constraint animations to the new path.
1722 | AnimationClip[] targetClips = allClips.Where(x =>
1723 | AnimationUtility.GetCurveBindings(x)
1724 | .Any(y => y.type == vrcComponents[j].GetType() && y.path == vrcTargetPath)).ToArray();
1725 | foreach (AnimationClip animationClip in targetClips)
1726 | {
1727 | EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
1728 | for (var bi = 0; bi < curveBindings.Length; bi++)
1729 | {
1730 | var curveBinding = curveBindings[bi];
1731 | AnimationCurve curve = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
1732 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, null);
1733 | if (curveBinding.type == vrcComponents[j].GetType() && curveBinding.path == vrcTargetPath)
1734 | {
1735 | curveBinding.path = newPath;
1736 | }
1737 | AnimationUtility.SetEditorCurve(animationClip, curveBinding, curve);
1738 | }
1739 | }
1740 | MoveVRChatConstraint(vrcComponents[j], userObject.gameObject);
1741 | }
1742 |
1743 | DestroyImmediate(target.gameObject);
1744 |
1745 | }
1746 | DestroyImmediate(prefab.gameObject);
1747 | }
1748 |
1749 | EditorUtility.DisplayDialog("Success!", "Custom Object Sync has been successfully removed", "Ok");
1750 | }
1751 |
1752 | public string GetDescriptorPath(Transform obj)
1753 | {
1754 | VRCAvatarDescriptor descriptor = obj.GetComponentInParent();
1755 | return AnimationUtility.CalculateTransformPath(obj.transform, descriptor.transform);
1756 | }
1757 |
1758 | public string GetDescriptorPath(GameObject obj) => GetDescriptorPath(obj.transform);
1759 |
1760 | public int GetMaxBitCount()
1761 | {
1762 | int rotationBits = rotationEnabled ? (rotationPrecision * 3) : 0;
1763 | int positionBits = 3 * (maxRadius + positionPrecision);
1764 | int maxbitCount = rotationBits + positionBits;
1765 | return maxbitCount;
1766 | }
1767 |
1768 | public int GetStepCount(int bits = -1)
1769 | {
1770 | bits = bits == -1 ? bitCount : bits;
1771 | return Mathf.CeilToInt(GetMaxBitCount() / (float)bits);
1772 | }
1773 |
1774 | public AnimatorStateTransition GenerateBitTransition(bool[] values, int index, int objectIndex, string parameterName, AnimatorState destinationState, bool read)
1775 | {
1776 | AnimatorStateTransition transition = null;
1777 | if (read)
1778 | {
1779 | transition =
1780 | GenerateTransition("", destinationState: destinationState, conditions: Enumerable.Range(0, values.Length).Select(i =>
1781 | GenerateCondition(
1782 | values[i] ? AnimatorConditionMode.Greater : AnimatorConditionMode.Less,
1783 | $"CustomObjectSync/Temp/{parameterName}{axis[i]}/{objectIndex}" ,
1784 | Mathf.Pow(0.5f, index + 1) * (values[i] ? 0.9999f : 1.0001f))
1785 | ).ToArray());
1786 | }
1787 | else
1788 | {
1789 | transition =
1790 | GenerateTransition("", destinationState: destinationState, conditions: Enumerable.Range(0, values.Length).Select(i =>
1791 | GenerateCondition(
1792 | values[i] ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot,
1793 | $"CustomObjectSync/Bits/{parameterName}{axis[i]}{index}/{objectIndex}" ,
1794 | 0)
1795 | ).ToArray());
1796 | }
1797 |
1798 |
1799 |
1800 | return transition;
1801 | }
1802 |
1803 |
1804 | bool[][] GeneratePermutations(int size)
1805 | {
1806 | return Enumerable.Range(0, (int)Math.Pow(2, size)).Select(i => Enumerable.Range(0, size).Select(b => ((i & (1 << b)) > 0)).ToArray()).ToArray();
1807 | }
1808 |
1809 | public bool ObjectPredicate(Func predicate, bool any = true, bool ignoreNulls = true)
1810 | {
1811 | return any ? syncObjects.Where(x => !ignoreNulls || x != null).Any(predicate) : syncObjects.Where(x => !ignoreNulls || x != null).All(predicate);
1812 | }
1813 |
1814 | public static void RenameClipPaths(AnimationClip[] clips, bool replaceEntire, string oldPath, string newPath)
1815 | {
1816 | try
1817 | {
1818 | AssetDatabase.StartAssetEditing();
1819 |
1820 | foreach (AnimationClip clip in clips)
1821 | {
1822 | EditorCurveBinding[] floatCurves = AnimationUtility.GetCurveBindings(clip);
1823 | EditorCurveBinding[] objectCurves = AnimationUtility.GetObjectReferenceCurveBindings(clip);
1824 |
1825 | foreach (EditorCurveBinding binding in floatCurves) ChangeBindings(binding, false);
1826 | foreach (EditorCurveBinding binding in objectCurves) ChangeBindings(binding, true);
1827 |
1828 | void ChangeBindings(EditorCurveBinding binding, bool isObjectCurve)
1829 | {
1830 | if (isObjectCurve)
1831 | {
1832 | ObjectReferenceKeyframe[] objectCurve = AnimationUtility.GetObjectReferenceCurve(clip, binding);
1833 |
1834 | if (!replaceEntire && binding.path.StartsWith(oldPath))
1835 | {
1836 | AnimationUtility.SetObjectReferenceCurve(clip, binding, null);
1837 | binding.path = binding.path.Replace(oldPath, newPath);
1838 | AnimationUtility.SetObjectReferenceCurve(clip, binding, objectCurve);
1839 | }
1840 |
1841 | if (replaceEntire && binding.path == oldPath)
1842 | {
1843 | AnimationUtility.SetObjectReferenceCurve(clip, binding, null);
1844 | binding.path = newPath;
1845 | AnimationUtility.SetObjectReferenceCurve(clip, binding, objectCurve);
1846 | }
1847 | }
1848 | else
1849 | {
1850 | AnimationCurve floatCurve = AnimationUtility.GetEditorCurve(clip, binding);
1851 |
1852 | if (!replaceEntire && binding.path.StartsWith(oldPath))
1853 | {
1854 | AnimationUtility.SetEditorCurve(clip, binding, null);
1855 | binding.path = binding.path.Replace(oldPath, newPath);
1856 | AnimationUtility.SetEditorCurve(clip, binding, floatCurve);
1857 | }
1858 |
1859 | if (replaceEntire && binding.path == oldPath)
1860 | {
1861 | AnimationUtility.SetEditorCurve(clip, binding, null);
1862 | binding.path = newPath;
1863 | AnimationUtility.SetEditorCurve(clip, binding, floatCurve);
1864 | }
1865 | }
1866 | }
1867 | }
1868 | }
1869 | finally { AssetDatabase.StopAssetEditing(); }
1870 | }
1871 |
1872 | public VRCExpressionsMenu GetMenuFromLocation(VRCAvatarDescriptor descriptor, string location)
1873 | {
1874 | VRCExpressionsMenu menu = descriptor.expressionsMenu;
1875 | if (location.StartsWith("/"))
1876 | {
1877 | location = location.Substring(1);
1878 | }
1879 | if (location.EndsWith("/"))
1880 | {
1881 | location = location.Substring(0, location.Length - 1);
1882 | }
1883 |
1884 | string[] menus = location.Split('/');
1885 |
1886 | if (menus.Length == 1 && menus[0] == "") return menu;
1887 |
1888 | for (int i = 0; i < menus.Length; i++)
1889 | {
1890 | string nextMenu = menus[i];
1891 | if (menu.controls == null) return null; // Menu's fucked up
1892 |
1893 | VRCExpressionsMenu.Control nextMenuControl = menu.controls.Where(x => x.type == VRCExpressionsMenu.Control.ControlType.SubMenu).FirstOrDefault(x => x.name == nextMenu);
1894 | if (nextMenuControl == null || nextMenuControl.subMenu == null) return null; // Menu not found
1895 |
1896 | menu = nextMenuControl.subMenu;
1897 | }
1898 |
1899 | if (menu.controls == null) return null; // Menu's fucked up
1900 | return menu;
1901 | }
1902 |
1903 | }
1904 | }
--------------------------------------------------------------------------------