├── .github └── workflows │ └── VRC-Asset-Release-And-Upload.yml ├── .gitignore ├── Editor.meta ├── Editor ├── ControllerGenerationMethods.cs ├── ControllerGenerationMethods.cs.meta ├── CustomObjectSyncCreator.cs ├── CustomObjectSyncCreator.cs.meta ├── CustomObjectSyncCreatorWindow.cs ├── CustomObjectSyncCreatorWindow.cs.meta ├── VRLabs.CustomObjectSync.Editor.asmdef └── VRLabs.CustomObjectSync.Editor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── Media.meta ├── Media ├── Preview.gif ├── Preview.png ├── Web │ ├── Preview.webp │ ├── Preview.webp.meta │ ├── PreviewGif.webp │ └── PreviewGif.webp.meta └── setup.mp4 ├── README.md ├── README.md.meta ├── Resources.meta ├── Resources ├── Custom Object Sync.prefab ├── Custom Object Sync.prefab.meta ├── World.prefab └── World.prefab.meta ├── package.json └── package.json.meta /.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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .github/.DS_Store 2 | .DS_Store 3 | /Media/*.meta 4 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9437062bf141d2647b0eee3beb86c4ba 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /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/ControllerGenerationMethods.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ac587a32b2e4f6b8b078567ba21359e 3 | timeCreated: 1704890805 -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /Editor/CustomObjectSyncCreator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35869b620c524f878a28a6fe04b09609 3 | timeCreated: 1704888691 -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /Editor/VRLabs.CustomObjectSync.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0e09ccbc2ed3564b9b844434974a6e9 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 42ae2f3f247ea65418d9fb4dfc41d92d 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Media.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 551f146e56004d50abcfa909035699ef 3 | timeCreated: 1714670814 -------------------------------------------------------------------------------- /Media/Preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/33451a67278f05743ee0be5435208193c35ebbc9/Media/Preview.gif -------------------------------------------------------------------------------- /Media/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/33451a67278f05743ee0be5435208193c35ebbc9/Media/Preview.png -------------------------------------------------------------------------------- /Media/Web/Preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/33451a67278f05743ee0be5435208193c35ebbc9/Media/Web/Preview.webp -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/33451a67278f05743ee0be5435208193c35ebbc9/Media/Web/PreviewGif.webp -------------------------------------------------------------------------------- /Media/Web/PreviewGif.webp.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7057abf8c46c4e246b7be20d133cdafb 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Media/setup.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VRLabs/Custom-Object-Sync/33451a67278f05743ee0be5435208193c35ebbc9/Media/setup.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Custom Object Sync 4 | 5 | [![Generic badge](https://img.shields.io/github/downloads/VRLabs/Custom-Object-Sync/total?label=Downloads)](https://github.com/VRLabs/Custom-Object-Sync/releases/latest) 6 | [![Generic badge](https://img.shields.io/badge/License-MIT-informational.svg)](https://github.com/VRLabs/Custom-Object-Sync/blob/main/LICENSE) 7 | [![Generic badge](https://img.shields.io/badge/Quest-Compatible-green?logo=Meta)](https://img.shields.io/badge/Quest-Compatible-green?logo=Meta) 8 | [![Generic badge](https://img.shields.io/badge/Unity-2022.3.22f1-lightblue?logo=Unity)](https://unity.com/releases/editor/whats-new/2022.3.22) 9 | [![Generic badge](https://img.shields.io/badge/SDK-AvatarSDK3-lightblue.svg)](https://vrchat.com/home/download) 10 | 11 | [![Generic badge](https://img.shields.io/discord/706913824607043605?color=%237289da&label=DISCORD&logo=Discord&style=for-the-badge)](https://discord.vrlabs.dev/) 12 | [![Generic badge](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dvrlabs%26type%3Dpatrons&style=for-the-badge)](https://patreon.vrlabs.dev/) 13 | 14 | Sync objects across the network with custom range, precision, and parameter usage. 15 | 16 | ![Preview](https://github.com/VRLabs/Custom-Object-Sync/assets/76777936/eb729e1f-d590-4810-bc3a-88df6915ed6a) 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 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9a851e68ea284bea8e3cb975608bbe1e 3 | timeCreated: 1714671136 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Resources/World.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 321681d27af33d04680c0839c27d6a19 3 | PrefabImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3f2e4b645fa1da64f873ae219f12ab4a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------