Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,9 @@ settings-general-fk_settings-enforce_joint_constraints-correct_constraints-descr
settings-general-fk_settings-ik = Position data
settings-general-fk_settings-ik-use_position = Use Position data
settings-general-fk_settings-ik-use_position-description = Enables the use of position data from trackers that provide it. When enabling this make sure to full reset and recalibrate in game.
settings-general-fk_settings-velocity_settings = Velocity Settings
settings-general-fk_settings-velocity_settings-description = Send derived velocity data to SteamVR. Required for Natural Locomotion support.
settings-general-fk_settings-velocity_settings-send_derived_velocity = Send derived velocity to driver
settings-general-fk_settings-arm_fk = Arm tracking
settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the headset (HMD) even if positional hand data is available.
settings-general-fk_settings-arm_fk-force_arms = Force arms from HMD
Expand Down
42 changes: 42 additions & 0 deletions gui/src/components/settings/pages/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SteamVRTrackersSettingT,
TapDetectionSettingsT,
HIDSettingsT,
VelocitySettingsT,
} from 'solarxr-protocol';
import { useConfig } from '@/hooks/config';
import { useWebsocketAPI } from '@/hooks/websocket-api';
Expand Down Expand Up @@ -105,6 +106,9 @@ export type SettingsForm = {
hidSettings: {
trackersOverHID: boolean;
};
velocitySettings: {
sendDerivedVelocity: boolean;
};
};

const defaultValues: SettingsForm = {
Expand Down Expand Up @@ -161,6 +165,7 @@ const defaultValues: SettingsForm = {
resetsSettings: defaultResetSettings,
stayAligned: defaultStayAlignedSettings,
hidSettings: { trackersOverHID: false },
velocitySettings: { sendDerivedVelocity: false },
};

export function GeneralSettings() {
Expand Down Expand Up @@ -286,6 +291,11 @@ export function GeneralSettings() {
hidSettings.trackersOverHid = values.hidSettings.trackersOverHID;
settings.hidSettings = hidSettings;

const velocitySettings = new VelocitySettingsT();
velocitySettings.sendDerivedVelocity =
values.velocitySettings.sendDerivedVelocity;
settings.velocitySettings = velocitySettings;

if (values.resetsSettings) {
settings.resetsSettings = loadResetSettings(values.resetsSettings);
}
Expand Down Expand Up @@ -407,6 +417,12 @@ export function GeneralSettings() {
};
}

if (settings.velocitySettings) {
formData.velocitySettings = {
sendDerivedVelocity: settings.velocitySettings.sendDerivedVelocity,
};
}

reset({ ...getValues(), ...formData });
});

Expand Down Expand Up @@ -994,6 +1010,32 @@ export function GeneralSettings() {
/>
</div>

<div className="flex flex-col pt-2 pb-1">
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-velocity_settings'
)}
</Typography>
<div className="pt-2">
<Typography>
{l10n.getString(
'settings-general-fk_settings-velocity_settings-description'
)}
</Typography>
</div>
</div>
<div className="grid sm:grid-cols-1 pb-3">
<CheckBox
variant="toggle"
outlined
control={control}
name="velocitySettings.sendDerivedVelocity"
label={l10n.getString(
'settings-general-fk_settings-velocity_settings-send_derived_velocity'
)}
/>
</div>

{config?.debug && (
<>
<div className="flex flex-col pt-2 pb-3">
Expand Down
18 changes: 18 additions & 0 deletions server/core/src/main/java/dev/slimevr/VRServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ class VRServer @JvmOverloads constructor(
instance = this
}

/**
* TODO: The design of this method is chosen for future expandability in case we want to have more complex velocity policies that depend on tracker properties or other config values.
* Initiates the velocity policy application process for the specified [Tracker] (or all trackers if null).
*
* This method serves as the initiator.
* The actual policy logic is not handled here,
* but is delegated to [dev.slimevr.config.VRConfig.applyVelocityPolicy].
*/
private fun applyVelocityPolicyTo(tracker: Tracker?) {
val targets = tracker?.let { listOf(it) } ?: trackers
for (t in targets) {
configManager.vrConfig.applyVelocityPolicy(t)
}
}

fun hasBridge(bridgeClass: Class<out Bridge?>): Boolean {
for (bridge in bridges) {
if (bridgeClass.isAssignableFrom(bridge.javaClass)) {
Expand Down Expand Up @@ -226,6 +241,9 @@ class VRServer @JvmOverloads constructor(
refreshTrackersDriftCompensationEnabled()
configManager.vrConfig.writeTrackerConfig(tracker)
configManager.saveConfig()

// Requires a fresh TrackerConfig on Update, so executed after we save a new state.
applyVelocityPolicyTo(tracker)
}
}

Expand Down
8 changes: 8 additions & 0 deletions server/core/src/main/java/dev/slimevr/config/VRConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class VRConfig {

val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig()

val velocityConfig: VelocityConfig = VelocityConfig()

val vrcConfig: VRCConfig = VRCConfig()

init {
Expand Down Expand Up @@ -104,6 +106,11 @@ class VRConfig {
return config
}

/** Resolves value from global config and applies to tracker. */
fun applyVelocityPolicy(tracker: Tracker) {
tracker.allowVelocity = velocityConfig.sendDerivedVelocity
}

fun readTrackerConfig(tracker: Tracker) {
if (tracker.userEditable) {
val config = getTracker(tracker)
Expand All @@ -119,6 +126,7 @@ class VRConfig {
.readFilteringConfig(filters, tracker.getRotation())
}
}
applyVelocityPolicy(tracker)
}

fun writeTrackerConfig(tracker: Tracker?) {
Expand Down
19 changes: 19 additions & 0 deletions server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.slimevr.config

import dev.slimevr.VRServer

/**
* Allows to enable/disable sending of optional derived velocity data via Protobuf.
* Enables Natural Locomotion Support
* May create overprediction in certain titles causing excessive jitter when moving upper body.
*/
class VelocityConfig {
// Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message.
var sendDerivedVelocity: Boolean = false

fun updateTrackersVelocityPolicy() {
for (t in VRServer.instance.allTrackers) {
t.allowVelocity = sendDerivedVelocity
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.slimevr.config.StayAlignedConfig
import dev.slimevr.config.TapDetectionConfig
import dev.slimevr.config.VMCConfig
import dev.slimevr.config.VRCOSCConfig
import dev.slimevr.config.VelocityConfig
import dev.slimevr.filtering.TrackerFilters.Companion.getByConfigkey
import dev.slimevr.tracking.processor.HumanPoseManager
import dev.slimevr.tracking.processor.config.SkeletonConfigToggles
Expand All @@ -34,6 +35,7 @@ import solarxr_protocol.rpc.SteamVRTrackersSetting
import solarxr_protocol.rpc.TapDetectionSettings
import solarxr_protocol.rpc.VMCOSCSettings
import solarxr_protocol.rpc.VRCOSCSettings
import solarxr_protocol.rpc.VelocitySettings
import solarxr_protocol.rpc.settings.LegTweaksSettings
import solarxr_protocol.rpc.settings.ModelRatios
import solarxr_protocol.rpc.settings.ModelSettings
Expand Down Expand Up @@ -421,6 +423,7 @@ fun createSettingsResponse(fbb: FlatBufferBuilder, server: VRServer): Int {
server.configManager.vrConfig.stayAlignedConfig,
),
createHIDSettings(fbb, server.configManager.vrConfig.hidConfig),
createVelocitySettings(fbb, server.configManager.vrConfig.velocityConfig),
)
}

Expand Down Expand Up @@ -456,3 +459,12 @@ fun createHIDSettings(
fbb,
config.trackersOverHID,
)

fun createVelocitySettings(
fbb: FlatBufferBuilder,
config: VelocityConfig,
): Int = VelocitySettings
.createVelocitySettings(
fbb,
config.sendDerivedVelocity,
)
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
config.trackersOverHID = requestConfig.trackersOverHid()
}

if (req.velocitySettings() != null) {
val velocityConfig = api.server.configManager.vrConfig.velocityConfig
velocityConfig.sendDerivedVelocity = req.velocitySettings().sendDerivedVelocity()
velocityConfig.updateTrackersVelocityPolicy()
}

api.server.configManager.saveConfig()
}

Expand All @@ -376,7 +382,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
val settings = SettingsResponse
.createSettingsResponse(
fbb,
createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
)
val outbound =
rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,7 @@ class HumanSkeleton(
it.position = trackerBone.getTailPosition()
it.setRotation(trackerBone.getGlobalRotation() * trackerBone.rotationOffset.inv())
it.dataTick()
it.updateDerivedVelocity(System.nanoTime())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ class Tracker @JvmOverloads constructor(
*/
val allowMounting: Boolean = false,

/**
* If true, the tracker will send Derived Velocity.
*/
var allowVelocity: Boolean = false,

val isHmd: Boolean = false,

/**
Expand Down Expand Up @@ -105,7 +110,19 @@ class Tracker @JvmOverloads constructor(
// IMU: +z forward, +x left, +y up
// SlimeVR: +z backward, +x right, +y up
private var _acceleration = Vector3.NULL
private var _velocity = Vector3.NULL
private var _magVector = Vector3.NULL

/**
* Velocity state server-side differentiation based on sent poses
*/
private data class VelocityState(
var prevTimeNs: Long = 0L,
var prevPos: Vector3 = Vector3(0f, 0f, 0f),
)

private var velocityState: VelocityState? = null

var position = Vector3.NULL
val resetsHandler: TrackerResetsHandler = TrackerResetsHandler(this)
val filteringHandler: TrackerFilteringHandler = TrackerFilteringHandler()
Expand Down Expand Up @@ -329,6 +346,42 @@ class Tracker @JvmOverloads constructor(
}
}

/**
* Updates the derived velocity of the tracker by differentiating position over time.
*
* This method enforces the [allowVelocity] policy and checks for valid position data before
* proceeding. If conditions are met, it calculates velocity based on the displacement since the
* last update, applying a sanity check on the time delta to filter out noise and ensure data stability.
*
*/
fun updateDerivedVelocity(nowNs: Long) {
if (!allowVelocity || !hasPosition) {
velocityState = null
_velocity = Vector3.NULL
return
}

val pos = position
val state = velocityState ?: VelocityState().also {
velocityState = it
}

if (state.prevTimeNs != 0L) {
val dt = (nowNs - state.prevTimeNs) * 1e-9
if (dt in 1e-4..0.25) {
_velocity = Vector3(
((pos.x - state.prevPos.x) / dt).toFloat(),
((pos.y - state.prevPos.y) / dt).toFloat(),
((pos.z - state.prevPos.z) / dt).toFloat(),
)
} else {
_velocity = Vector3.NULL
}
}
state.prevTimeNs = nowNs
state.prevPos = pos
}

/**
* Gets the identity-adjusted tracker rotation after the resetsHandler's corrections
* (identity reset, drift and identity mounting).
Expand Down Expand Up @@ -405,6 +458,24 @@ class Tracker @JvmOverloads constructor(
this._acceleration = vec
}

/**
* Sets the derived velocity of the tracker.
*/
fun setVelocity(vec: Vector3) {
this._velocity = if (allowVelocity) vec else Vector3.NULL
}

/**
* Gets the derived velocity of the tracker.
*/
fun getVelocity(): Vector3 = _velocity

/**
* True if the tracker has valid velocity data.
*/
val hasVelocity: Boolean
get() = allowVelocity && _velocity != Vector3.NULL

/**
* True if the raw rotation is coming directly from an IMU (no cameras or lighthouses)
* For example, flex sensor trackers are not considered as IMU trackers (see TrackerDataType)
Expand Down
Loading