From d8d67fbb63f7f2cd5d9215b47c60119b978be2f8 Mon Sep 17 00:00:00 2001 From: Erimel Date: Sat, 18 May 2024 13:38:55 -0400 Subject: [PATCH] Add toggle to mirror tracking --- gui/public/i18n/en/translation.ftl | 3 + .../components/settings/pages/VMCSettings.tsx | 22 ++++++ .../main/java/dev/slimevr/config/VMCConfig.kt | 3 + .../main/java/dev/slimevr/osc/VMCHandler.kt | 73 ++++++++++++------- .../rpc/settings/RPCSettingsBuilder.java | 1 + .../rpc/settings/RPCSettingsHandler.kt | 1 + solarxr-protocol | 2 +- 7 files changed, 77 insertions(+), 28 deletions(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index ce4026ce68..f5af2ddf37 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -553,6 +553,9 @@ settings-osc-vmc-vrm-file_select = Drag & drop a model to use, or browse settings-osc-vmc-anchor_hip = Anchor at hips settings-osc-vmc-anchor_hip-description = Anchor the tracking at the hips, useful for seated VTubing. If disabling, load a VRM model. settings-osc-vmc-anchor_hip-label = Anchor at hips +settings-osc-vmc-mirror_tracking = Mirror tracking +settings-osc-vmc-mirror_tracking-description = Mirror the tracking horizontally. +settings-osc-vmc-mirror_tracking-label = Mirror tracking ## Setup/onboarding menu onboarding-skip = Skip setup diff --git a/gui/src/components/settings/pages/VMCSettings.tsx b/gui/src/components/settings/pages/VMCSettings.tsx index 06521ea0cf..a6d2d6cc80 100644 --- a/gui/src/components/settings/pages/VMCSettings.tsx +++ b/gui/src/components/settings/pages/VMCSettings.tsx @@ -32,6 +32,7 @@ interface VMCSettingsForm { }; vrmJson?: FileList; anchorHip: boolean; + mirrorTracking: boolean; }; } @@ -44,6 +45,7 @@ const defaultValues = { address: '127.0.0.1', }, anchorHip: true, + mirrorTracking: true, }, }; @@ -80,6 +82,7 @@ export function VMCSettings() { } } vmcOsc.anchorHip = values.vmc.anchorHip; + vmcOsc.mirrorTracking = values.vmc.mirrorTracking; settings.vmcOsc = vmcOsc; } @@ -115,6 +118,7 @@ export function VMCSettings() { } formData.vmc.anchorHip = settings.vmcOsc.anchorHip; + formData.vmc.mirrorTracking = settings.vmcOsc.mirrorTracking; } reset(formData); @@ -272,6 +276,23 @@ export function VMCSettings() { label={l10n.getString('settings-osc-vmc-anchor_hip-label')} /> + + {l10n.getString('settings-osc-vmc-mirror_tracking')} + +
+ + {l10n.getString('settings-osc-vmc-mirror_tracking-description')} + +
+
+ +
@@ -281,6 +302,7 @@ export function VMCSettings() { const gltfHeaderStart = 0; const gltfHeaderEnd = 20; + async function parseVRMFile(vrm: File): Promise { const headerView = new DataView( await vrm.slice(gltfHeaderStart, gltfHeaderEnd).arrayBuffer() diff --git a/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt b/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt index a424cc525c..b48e4d70a2 100644 --- a/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt @@ -7,4 +7,7 @@ class VMCConfig : OSCConfig() { // JSON part of the VRM to be used var vrmJson: String? = null + + // Mirror the tracking before sending it (turn left <=> turn right, left leg <=> right leg) + var mirrorTracking = false } diff --git a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt index 731d9105dc..282e2e3bbe 100644 --- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt @@ -59,6 +59,7 @@ class VMCHandler( private var timeAtLastError: Long = 0 private var timeAtLastSend: Long = 0 private var anchorHip = false + private var mirrorTracking = false private var lastPortIn = 0 private var lastPortOut = 0 private var lastAddress: InetAddress? = null @@ -69,6 +70,7 @@ class VMCHandler( override fun refreshSettings(refreshRouterSettings: Boolean) { anchorHip = config.anchorHip + mirrorTracking = config.mirrorTracking // Stops listening and closes OSC port val wasListening = oscReceiver != null && oscReceiver!!.isListening @@ -346,17 +348,23 @@ class VMCHandler( ) for (unityBone in UnityBone.entries) { - val boneType = unityBone.boneType ?: continue + if (unityBone.boneType == null) continue + + // Get opposite bone if tracking must be mirrored + val boneType = (if (mirrorTracking) tryGetOppositeArmBone(unityBone) else unityBone).boneType + // Get SlimeVR bone - val bone = humanPoseManager.getBone(boneType) - - // Update unity hierarchy from bone's global - // rotation - outputUnityArmature - ?.setGlobalRotationForBone( - unityBone, - bone!!.getGlobalRotation() * bone.rotationOffset.inv(), - ) + val bone = humanPoseManager.getBone(boneType!!) + + // Update unity hierarchy from bone's global rotation + val boneRotation = if (mirrorTracking) { + // Mirror tracking horizontally + val rotBuf = bone.getGlobalRotation() * bone.rotationOffset.inv() + Quaternion(rotBuf.w, rotBuf.x, -rotBuf.y, -rotBuf.z) + } else { + bone.getGlobalRotation() * bone.rotationOffset.inv() + } + outputUnityArmature?.setGlobalRotationForBone(unityBone, boneRotation) } if (!anchorHip) { // Anchor from head @@ -405,14 +413,9 @@ class VMCHandler( // Add Unity humanoid bones transforms for (bone in UnityBone.entries) { - if (bone.boneType != null && !( - humanPoseManager.isTrackingLeftArmFromController && - isLeftArmUnityBone(bone) - ) && - !( - humanPoseManager.isTrackingRightArmFromController && - isRightArmUnityBone(bone) - ) + if (bone.boneType != null && + !(humanPoseManager.isTrackingLeftArmFromController && isLeftArmUnityBone(bone)) && + !(humanPoseManager.isTrackingRightArmFromController && isRightArmUnityBone(bone)) ) { oscArgs.clear() oscArgs.add(bone.stringVal) @@ -420,13 +423,7 @@ class VMCHandler( outputUnityArmature!!.getLocalTranslationForBone(bone), outputUnityArmature!!.getLocalRotationForBone(bone), ) - oscBundle - .addPacket( - OSCMessage( - "/VMC/Ext/Bone/Pos", - oscArgs.clone(), - ), - ) + oscBundle.addPacket(OSCMessage("/VMC/Ext/Bone/Pos", oscArgs.clone())) } } } @@ -519,9 +516,31 @@ class VMCHandler( oscArgs.add(-rot.w) } - private fun isLeftArmUnityBone(bone: UnityBone): Boolean = bone == UnityBone.LEFT_UPPER_ARM || bone == UnityBone.LEFT_LOWER_ARM || bone == UnityBone.LEFT_HAND + /** + * Returns the bone on the opposite limb, or the original bone if + * it not a limb bone. + */ + private fun tryGetOppositeArmBone(bone: UnityBone): UnityBone = when (bone) { + UnityBone.LEFT_SHOULDER -> UnityBone.RIGHT_SHOULDER + UnityBone.LEFT_UPPER_ARM -> UnityBone.RIGHT_UPPER_ARM + UnityBone.LEFT_LOWER_ARM -> UnityBone.RIGHT_LOWER_ARM + UnityBone.LEFT_HAND -> UnityBone.RIGHT_HAND + UnityBone.RIGHT_SHOULDER -> UnityBone.LEFT_SHOULDER + UnityBone.RIGHT_UPPER_ARM -> UnityBone.LEFT_UPPER_ARM + UnityBone.RIGHT_LOWER_ARM -> UnityBone.LEFT_LOWER_ARM + UnityBone.RIGHT_HAND -> UnityBone.LEFT_HAND + UnityBone.LEFT_UPPER_LEG -> UnityBone.RIGHT_UPPER_LEG + UnityBone.LEFT_LOWER_LEG -> UnityBone.RIGHT_LOWER_LEG + UnityBone.LEFT_FOOT -> UnityBone.RIGHT_FOOT + UnityBone.RIGHT_UPPER_LEG -> UnityBone.LEFT_UPPER_LEG + UnityBone.RIGHT_LOWER_LEG -> UnityBone.LEFT_LOWER_LEG + UnityBone.RIGHT_FOOT -> UnityBone.LEFT_FOOT + else -> bone + } + + private fun isLeftArmUnityBone(bone: UnityBone): Boolean = bone == UnityBone.LEFT_SHOULDER || bone == UnityBone.LEFT_UPPER_ARM || bone == UnityBone.LEFT_LOWER_ARM || bone == UnityBone.LEFT_HAND - private fun isRightArmUnityBone(bone: UnityBone): Boolean = bone == UnityBone.RIGHT_UPPER_ARM || bone == UnityBone.RIGHT_LOWER_ARM || bone == UnityBone.RIGHT_HAND + private fun isRightArmUnityBone(bone: UnityBone): Boolean = bone == UnityBone.RIGHT_SHOULDER || bone == UnityBone.RIGHT_UPPER_ARM || bone == UnityBone.RIGHT_LOWER_ARM || bone == UnityBone.RIGHT_HAND override fun getOscSender(): OSCPortOut = oscSender!! diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java index eb5e28f475..b2d5fe8beb 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java @@ -98,6 +98,7 @@ public static int createVMCOSCSettings( if (vrmJson != null) VMCOSCSettings.addVrmJson(fbb, vrmJsonOffset); VMCOSCSettings.addAnchorHip(fbb, config.getAnchorHip()); + VMCOSCSettings.addMirrorTracking(fbb, config.getMirrorTracking()); return VMCOSCSettings.endVMCOSCSettings(fbb); } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 8ca8d79dba..4d7b18ec86 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -151,6 +151,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { } if (req.vmcOsc().vrmJson() != null) vmcConfig.vrmJson = req.vmcOsc().vrmJson() vmcConfig.anchorHip = req.vmcOsc().anchorHip() + vmcConfig.mirrorTracking = req.vmcOsc().mirrorTracking() vmcHandler.refreshSettings(true) } diff --git a/solarxr-protocol b/solarxr-protocol index b1ae56c26a..f44a97989d 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit b1ae56c26a7b1e262bf051589620433de1eea88f +Subproject commit f44a97989d167723db01fdcfecf446f3da9b651e