diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl
index d8efac7a8c..a126aea2a2 100644
--- a/gui/public/i18n/en/translation.ftl
+++ b/gui/public/i18n/en/translation.ftl
@@ -42,6 +42,7 @@ body_part-LEFT_HAND = Left hand
body_part-LEFT_UPPER_LEG = Left thigh
body_part-LEFT_LOWER_LEG = Left ankle
body_part-LEFT_FOOT = Left foot
+body_part-ACCESSORY = Accessory
## Proportions
skeleton_bone-NONE = None
@@ -453,6 +454,8 @@ settings-osc-vrchat = VRChat OSC Trackers
settings-osc-vrchat-description =
Change VRChat-specific settings to receive HMD data and send
tracker data for FBT without SteamVR (ex. Quest standalone).
+ This also allows you to receive trackers under the OSCTrackers standard.
+ This will also send accessory trackers' rotations for custom avatars.
settings-osc-vrchat-enable = Enable
settings-osc-vrchat-enable-description = Toggle the sending and receiving of data.
settings-osc-vrchat-enable-label = Enable
@@ -480,7 +483,7 @@ settings-osc-vmc = Virtual Motion Capture
# This cares about multilines
settings-osc-vmc-description =
Change settings specific to the VMC (Virtual Motion Capture) protocol
- to send SlimeVR's bone data and receive bone data from other apps.
+ to send and received bone and tracker data from and to other apps.
settings-osc-vmc-enable = Enable
settings-osc-vmc-enable-description = Toggle the sending and receiving of data.
settings-osc-vmc-enable-label = Enable
diff --git a/gui/src/components/commons/BodyPartIcon.tsx b/gui/src/components/commons/BodyPartIcon.tsx
index 7463f312c3..f692a80b84 100644
--- a/gui/src/components/commons/BodyPartIcon.tsx
+++ b/gui/src/components/commons/BodyPartIcon.tsx
@@ -84,6 +84,9 @@ export const mapPart: Record<
),
[BodyPart.WAIST]: ({ width }) => ,
+ [BodyPart.ACCESSORY]: ({ width }) => (
+
+ ),
};
export function BodyPartIcon({
diff --git a/gui/src/components/onboarding/BodyAssignment.tsx b/gui/src/components/onboarding/BodyAssignment.tsx
index 3ebe3fbc96..3c51f248ad 100644
--- a/gui/src/components/onboarding/BodyAssignment.tsx
+++ b/gui/src/components/onboarding/BodyAssignment.tsx
@@ -293,6 +293,19 @@ export function BodyAssignment({
direction="left"
/>
+
+ {advanced && (
+
+ onRoleSelected(BodyPart.ACCESSORY)}
+ direction="left"
+ />
+
+ )}
}
>
diff --git a/gui/src/components/onboarding/pages/mounting/ManualMounting.tsx b/gui/src/components/onboarding/pages/mounting/ManualMounting.tsx
index 2565c9452d..ca30277a40 100644
--- a/gui/src/components/onboarding/pages/mounting/ManualMounting.tsx
+++ b/gui/src/components/onboarding/pages/mounting/ManualMounting.tsx
@@ -53,6 +53,7 @@ export function ManualMountingPage() {
assignreq.trackerId = td.tracker.trackerId;
assignreq.allowDriftCompensation =
td.tracker.info?.allowDriftCompensation ?? true;
+ assignreq.accessoryId = td.tracker.info?.accessoryId || 0;
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
});
diff --git a/gui/src/components/onboarding/pages/mounting/MountingSelectionMenu.tsx b/gui/src/components/onboarding/pages/mounting/MountingSelectionMenu.tsx
index 26596e9772..101f20b427 100644
--- a/gui/src/components/onboarding/pages/mounting/MountingSelectionMenu.tsx
+++ b/gui/src/components/onboarding/pages/mounting/MountingSelectionMenu.tsx
@@ -96,6 +96,7 @@ export const mapPart: Record<
),
[BodyPart.WAIST]: ({ width }) => ,
+ [BodyPart.ACCESSORY]: ({ width }) => ,
};
export function MountingBodyPartIcon({
diff --git a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx
index a410ce0977..40391a37a9 100644
--- a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx
+++ b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx
@@ -192,6 +192,7 @@ export function TrackersAssignPage() {
assignreq.trackerId = trackerId;
assignreq.allowDriftCompensation =
tracker?.tracker?.info?.allowDriftCompensation ?? true;
+ assignreq.accessoryId = 1; // TODO
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
};
diff --git a/gui/src/components/tracker/TrackerSettings.tsx b/gui/src/components/tracker/TrackerSettings.tsx
index 0284167651..00777ba5c9 100644
--- a/gui/src/components/tracker/TrackerSettings.tsx
+++ b/gui/src/components/tracker/TrackerSettings.tsx
@@ -92,6 +92,7 @@ export function TrackerSettingsPage() {
assignreq.trackerId = tracker?.tracker.trackerId;
if (allowDriftCompensation != null)
assignreq.allowDriftCompensation = allowDriftCompensation;
+ assignreq.accessoryId = 1; // TODO
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
setSelectBodypart(false);
};
diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt
index 8b7e2819aa..828ebd2f3f 100644
--- a/server/core/src/main/java/dev/slimevr/VRServer.kt
+++ b/server/core/src/main/java/dev/slimevr/VRServer.kt
@@ -59,7 +59,6 @@ class VRServer @JvmOverloads constructor(
val trackersServer: TrackersUDPServer
private val bridges: MutableList = FastList()
private val tasks: Queue = LinkedBlockingQueue()
- private val newTrackersConsumers: MutableList> = FastList()
private val onTick: MutableList = FastList()
val oSCRouter: OSCRouter
@@ -197,16 +196,6 @@ class VRServer @JvmOverloads constructor(
onTick.add(runnable)
}
- @ThreadSafe
- fun addNewTrackerConsumer(consumer: Consumer) {
- queueTask {
- newTrackersConsumers.add(consumer)
- for (tracker in trackers) {
- consumer.accept(tracker)
- }
- }
- }
-
@ThreadSafe
fun trackerUpdated(tracker: Tracker?) {
queueTask {
@@ -215,6 +204,11 @@ class VRServer @JvmOverloads constructor(
refreshTrackersDriftCompensationEnabled()
configManager.vrConfig.writeTrackerConfig(tracker)
configManager.saveConfig()
+ if (tracker != null) {
+ if (tracker.trackerPosition == TrackerPosition.ACCESSORY) {
+ vrcOSCHandler.addAccessoryTracker(tracker)
+ }
+ }
}
}
@@ -270,6 +264,9 @@ class VRServer @JvmOverloads constructor(
if (tracker.isComputed && tracker.name != "HMD") {
vMCHandler.addComputedTracker(tracker)
}
+ if (tracker.trackerPosition == TrackerPosition.ACCESSORY) {
+ vrcOSCHandler.addAccessoryTracker(tracker)
+ }
refreshTrackersDriftCompensationEnabled()
}
@@ -279,9 +276,6 @@ class VRServer @JvmOverloads constructor(
queueTask {
trackers.add(tracker)
trackerAdded(tracker)
- for (tc in newTrackersConsumers) {
- tc.accept(tracker)
- }
}
}
diff --git a/server/core/src/main/java/dev/slimevr/config/TrackerConfig.java b/server/core/src/main/java/dev/slimevr/config/TrackerConfig.java
deleted file mode 100644
index fd47a622c0..0000000000
--- a/server/core/src/main/java/dev/slimevr/config/TrackerConfig.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package dev.slimevr.config;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import dev.slimevr.tracking.trackers.Tracker;
-import io.github.axisangles.ktmath.Quaternion;
-
-
-public class TrackerConfig {
-
- private String customName;
- private String designation;
- private boolean hide;
- private Quaternion adjustment;
- private Quaternion mountingOrientation;
- private Boolean allowDriftCompensation;
-
-
- public TrackerConfig() {
- }
-
- public TrackerConfig(Tracker tracker) {
- this.designation = tracker.getTrackerPosition()
- != null ? tracker.getTrackerPosition().getDesignation() : null;
- this.customName = tracker.getCustomName();
- allowDriftCompensation = tracker.isImu();
- }
-
- static JsonNode toV2(JsonNode v1, JsonNodeFactory factory) {
- ObjectNode node = factory.objectNode();
- if (v1.has("customName"))
- node.set("customName", v1.get("customName"));
- if (v1.has("designation"))
- node.set("designation", v1.get("designation"));
- if (v1.has("hide"))
- node.set("hide", v1.get("hide"));
- if (v1.has("mountingRotation"))
- node.set("mountingRotation", v1.get("mountingRotation"));
- if (v1.has("adjustment"))
- node.set("adjustment", v1.get("adjustment"));
- return node;
- }
-
- public String getCustomName() {
- return customName;
- }
-
- public void setCustomName(String customName) {
- this.customName = customName;
- }
-
- public String getDesignation() {
- return designation;
- }
-
- public void setDesignation(String designation) {
- this.designation = designation;
- }
-
- public boolean isHide() {
- return hide;
- }
-
- public void setHide(boolean hide) {
- this.hide = hide;
- }
-
- public Quaternion getAdjustment() {
- return adjustment;
- }
-
- public void setAdjustment(Quaternion adjustment) {
- this.adjustment = adjustment;
- }
-
- public Quaternion getMountingOrientation() {
- return mountingOrientation;
- }
-
- public void setMountingOrientation(Quaternion mountingOrientation) {
- this.mountingOrientation = mountingOrientation;
- }
-
- public Boolean getAllowDriftCompensation() {
- return allowDriftCompensation;
- }
-
- public void setAllowDriftCompensation(Boolean allowDriftCompensation) {
- this.allowDriftCompensation = allowDriftCompensation;
- }
-}
diff --git a/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt b/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt
new file mode 100644
index 0000000000..52a69ba609
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt
@@ -0,0 +1,42 @@
+package dev.slimevr.config
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.JsonNodeFactory
+import dev.slimevr.tracking.trackers.Tracker
+import io.github.axisangles.ktmath.Quaternion
+
+class TrackerConfig {
+ var customName: String? = null
+ var designation: String? = null
+ var isHide = false
+ var adjustment: Quaternion? = null
+ var mountingOrientation: Quaternion? = null
+ var allowDriftCompensation: Boolean? = null
+ var accessoryId: Int? = null
+
+ constructor()
+ constructor(tracker: Tracker) {
+ designation = if (tracker.trackerPosition
+ != null
+ ) {
+ tracker.trackerPosition!!.designation
+ } else {
+ null
+ }
+ customName = tracker.customName
+ allowDriftCompensation = tracker.isImu()
+ }
+
+ companion object {
+ @JvmStatic
+ fun toV2(v1: JsonNode, factory: JsonNodeFactory): JsonNode {
+ val node = factory.objectNode()
+ if (v1.has("customName")) node.set("customName", v1["customName"])
+ if (v1.has("designation")) node.set("designation", v1["designation"])
+ if (v1.has("hide")) node.set("hide", v1["hide"])
+ if (v1.has("mountingRotation")) node.set("mountingRotation", v1["mountingRotation"])
+ if (v1.has("adjustment")) node.set("adjustment", v1["adjustment"])
+ return node
+ }
+ }
+}
diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
index b9c1d1feb3..3107674b2a 100644
--- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
@@ -28,7 +28,8 @@ import io.github.axisangles.ktmath.Vector3
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
-import java.util.*
+import java.util.Timer
+import java.util.TimerTask
private const val OFFSET_SLERP_FACTOR = 0.5f // Guessed from eyeing VRChat
@@ -63,6 +64,7 @@ class VRCOSCHandler(
private val postReceivingOffset = EulerAngles(EulerOrder.YXZ, 0f, FastMath.PI, 0f).toQuaternion()
private var timeAtLastReceivedRotationOffset = System.currentTimeMillis()
private var fpsTimer: NanoTimer? = null
+ private val accessoryTrackers: MutableList = FastList()
init {
refreshSettings(false)
@@ -329,6 +331,7 @@ class VRCOSCHandler(
// Create new bundle
val bundle = OSCBundle()
+ // Send computed trackers as OSC Trackers
for (i in computedTrackers.indices) {
if (trackersEnabled[i]) {
// Send regular trackers' positions
@@ -353,12 +356,7 @@ class VRCOSCHandler(
// Y quaternion represents a rotation from z to x
// When we negate the z direction, X and Y quaternion
// components must be negated.
- val (_, x2, y2, z2) = Quaternion(
- w,
- -x1,
- -y1,
- z1
- ).toEulerAngles(EulerOrder.YXZ)
+ val (_, x2, y2, z2) = Quaternion(w, -x1, -y1, z1).toEulerAngles(EulerOrder.YXZ)
oscArgs.clear()
oscArgs.add(x2 * FastMath.RAD_TO_DEG)
oscArgs.add(y2 * FastMath.RAD_TO_DEG)
@@ -386,6 +384,27 @@ class VRCOSCHandler(
}
}
+ // Send accessory trackers for custom avatars
+ for (i in accessoryTrackers.indices) {
+ if (accessoryTrackers[i].trackerPosition == TrackerPosition.ACCESSORY && accessoryTrackers[i].status.sendData) {
+ val (_, x, y, z) = accessoryTrackers[i].getRotation().toEulerAngles(EulerOrder.YZX)
+ // Note: we must send the rotations as -1 to 1
+ // https://creators.vrchat.com/avatars/animator-parameters/#parameter-types
+ // Horizontal
+ oscArgs.clear()
+ oscArgs.add(x / FastMath.PI)
+ bundle.addPacket(OSCMessage("/avatar/parameters/SVRAccessory${accessoryTrackers[i].accessoryId}X", oscArgs.clone()))
+ // Vertical
+ oscArgs.clear()
+ oscArgs.add(y / FastMath.PI)
+ bundle.addPacket(OSCMessage("/avatar/parameters/SVRAccessory${accessoryTrackers[i].accessoryId}Y", oscArgs.clone()))
+ // Twist
+ oscArgs.clear()
+ oscArgs.add(z / FastMath.PI)
+ bundle.addPacket(OSCMessage("/avatar/parameters/SVRAccessory${accessoryTrackers[i].accessoryId}Z", oscArgs.clone()))
+ }
+ }
+
try {
oscSender!!.send(bundle)
} catch (e: IOException) {
@@ -455,6 +474,17 @@ class VRCOSCHandler(
}
}
+ /**
+ * Adds an accessory tracker to the list of trackers to send.
+ *
+ * @param tracker the accessory tracker
+ */
+ fun addAccessoryTracker(tracker: Tracker) {
+ if (!accessoryTrackers.contains(tracker)) {
+ accessoryTrackers.add(tracker)
+ }
+ }
+
override fun getOscSender(): OSCPortOut {
return oscSender!!
}
diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.java b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.java
index 986582211e..85ccc65941 100644
--- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.java
+++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.java
@@ -270,6 +270,10 @@ public void onAssignTrackerRequest(GenericConnection conn, RpcMessageHeader mess
tracker.getResetsHandler().setAllowDriftCompensation(req.allowDriftCompensation());
}
+ if (req.accessoryId() != 0) {
+ tracker.setAccessoryId(req.accessoryId());
+ }
+
this.api.server.trackerUpdated(tracker);
}
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
index 8df1511b35..229e901008 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
@@ -77,6 +77,7 @@ class Tracker @JvmOverloads constructor(
var signalStrength: Int? = null
var temperature: Float? = null
var customName: String? = null
+ var accessoryId: Int? = null
/**
* If the tracker has gotten disconnected after it was initialized first time
@@ -209,6 +210,9 @@ class Tracker @JvmOverloads constructor(
config.customName?.let {
customName = it
}
+ config.accessoryId?.let {
+ accessoryId = it
+ }
config.designation?.let { designation ->
getByDesignation(designation)?.let { trackerPosition = it }
} ?: run { trackerPosition = null }
@@ -238,6 +242,7 @@ class Tracker @JvmOverloads constructor(
fun writeConfig(config: TrackerConfig) {
trackerPosition?.let { config.designation = it.designation } ?: run { config.designation = null }
customName?.let { config.customName = it }
+ accessoryId?.let { config.accessoryId = it }
if (needsMounting) {
config.mountingOrientation = resetsHandler.mountingOrientation
}
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt
index e814ca65bf..46b4fb06d7 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt
@@ -33,6 +33,7 @@ enum class TrackerPosition(
RIGHT_HAND("body:right_hand", TrackerRole.RIGHT_HAND, BodyPart.RIGHT_HAND),
LEFT_SHOULDER("body:left_shoulder", TrackerRole.LEFT_SHOULDER, BodyPart.LEFT_SHOULDER),
RIGHT_SHOULDER("body:right_shoulder", TrackerRole.RIGHT_SHOULDER, BodyPart.RIGHT_SHOULDER),
+ ACCESSORY("accessory", null, BodyPart.ACCESSORY),
;
companion object {
diff --git a/solarxr-protocol b/solarxr-protocol
index c22d4729ec..01631f99e4 160000
--- a/solarxr-protocol
+++ b/solarxr-protocol
@@ -1 +1 @@
-Subproject commit c22d4729ec45a9d4bd357458cfa4cc5cc65968ab
+Subproject commit 01631f99e43698b8b3a3a9c3bb62c91845a04237