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 @@ -635,6 +635,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. May cause jitter in FBT.
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 @@ -108,6 +109,9 @@ export type SettingsForm = {
hidSettings: {
trackersOverHID: boolean;
};
velocitySettings: {
sendDerivedVelocity: boolean;
};
};

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

const settingsAtom = atom(new SettingsResponseT());
Expand Down Expand Up @@ -325,6 +330,11 @@ export function GeneralSettings() {
hidSettings.trackersOverHid = values.hidSettings.trackersOverHID;
settingsReq.hidSettings = hidSettings;

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

if (values.resetsSettings) {
settingsReq.resetsSettings = loadResetSettings(values.resetsSettings);
}
Expand Down Expand Up @@ -446,6 +456,12 @@ export function GeneralSettings() {
};
}

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

reset({ ...getValues(), ...formData });
}, [settings]);

Expand Down Expand Up @@ -1016,6 +1032,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
3 changes: 3 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 @@ -119,6 +121,7 @@ class VRConfig {
.readFilteringConfig(filters, tracker.getRotation())
}
}
tracker.allowVelocity = velocityConfig.sendDerivedVelocity
}

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 updateTrackersVelocitySettings() {
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 @@ -427,6 +429,7 @@ fun createSettingsResponse(fbb: FlatBufferBuilder, server: VRServer): Int {
),
createHIDSettings(fbb, server.configManager.vrConfig.hidConfig),
0,
createVelocitySettings(fbb, server.configManager.vrConfig.velocityConfig),
)
}

Expand Down Expand Up @@ -462,3 +465,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 @@ -362,6 +362,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.updateTrackersVelocitySettings()
}

api.server.configManager.saveConfig()
}

Expand All @@ -379,7 +385,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, 0,
createSteamVRSettings(fbb, bridge), 0, 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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import io.eiren.util.BufferedTimer
import io.github.axisangles.ktmath.Quaternion
import io.github.axisangles.ktmath.Vector3
import kotlin.properties.Delegates
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlin.time.TimeSource

const val TIMEOUT_MS = 2_000L
const val DISCONNECT_MS = 3_000L + TIMEOUT_MS
Expand Down Expand Up @@ -75,6 +79,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 @@ -111,7 +120,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 prevMark: TimeSource.Monotonic.ValueTimeMark? = null,
var prevPos: Vector3 = Vector3(0f, 0f, 0f),
)

private val velocityState = VelocityState()

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

/**
* If derived velocity is enabled, updates the derived velocity of the tracker,
* calculating it based on the position displacement since the last update,
* applying a sanity check on the time delta to filter out noise and ensure data stability.
*/
Comment on lines +369 to +373
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be shortened

Suggested change
/**
* 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.
*
*/
/**
* If derived velocity is enabled, updates the derived velocity of the tracker, calculating it based on the position displacement
* since the last update, applying a sanity check on the time delta to filter out noise and ensure data stability.
*
*/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this is not showing up as an outdated comment, and also doesn't show up in the diff for the PR.
If looking directly at my base branch, you can see the update: https://github.com/M1DNYT3/SlimeVR-Server-NaLo-Support/blob/c9c750230ea73da399fbad0276fd919647b8bea8/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt#L369

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hell, even a rebase didn't help. Seems like an issue on GitHub's end.

Anyway, both comments are addressed and the branch is rebased to the latest main, so the merge should be clean now.

fun updateDerivedVelocity() {
if (!allowVelocity || !hasPosition) {
velocityState.prevMark = null
_velocity = Vector3.NULL
return
}

val pos = position
val now = TimeSource.Monotonic.markNow()

val prevMark = velocityState.prevMark
if (prevMark != null) {
val dt = now - prevMark
if (dt in 100.microseconds..250.milliseconds) {
val dtSeconds = dt.toDouble(DurationUnit.SECONDS)
_velocity = Vector3(
((pos.x - velocityState.prevPos.x) / dtSeconds).toFloat(),
((pos.y - velocityState.prevPos.y) / dtSeconds).toFloat(),
((pos.z - velocityState.prevPos.z) / dtSeconds).toFloat(),
)
} else {
_velocity = Vector3.NULL
}
}
velocityState.prevMark = now
velocityState.prevPos = pos
}

/**
* Gets the identity-adjusted tracker rotation after the resetsHandler's corrections
* (identity reset, drift and identity mounting).
Expand Down Expand Up @@ -421,6 +475,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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte
target.position = source.position
target.setRotation(source.getRotation())
target.status = source.status
target.setVelocity(source.getVelocity())
target.batteryLevel = source.batteryLevel
target.batteryVoltage = source.batteryVoltage
target.dataTick()
Expand All @@ -102,24 +103,37 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte
}

@VRServerThread
protected fun writeTrackerUpdate(localTracker: Tracker?) {
val builder = ProtobufMessages.Position.newBuilder().setTrackerId(
localTracker!!.id,
)
protected fun writeTrackerUpdate(localTracker: Tracker) {
val builder = ProtobufMessages.Position.newBuilder()
.setTrackerId(localTracker.id)

if (localTracker.hasPosition) {
val pos = localTracker.position
builder.setX(pos.x)
builder.setY(pos.y)
builder.setZ(pos.z)
}

if (localTracker.hasRotation) {
val rot = localTracker.getRotation()
builder.setQx(rot.x)
builder.setQy(rot.y)
builder.setQz(rot.z)
builder.setQw(rot.w)
}
sendMessage(ProtobufMessage.newBuilder().setPosition(builder).build())

if (localTracker.hasVelocity) {
val vel = localTracker.getVelocity()
builder.setVx(vel.x)
builder.setVy(vel.y)
builder.setVz(vel.z)
}

sendMessage(
ProtobufMessage.newBuilder()
.setPosition(builder)
.build(),
)
}

@VRServerThread
Expand Down Expand Up @@ -169,6 +183,16 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte
),

)
if (positionMessage.hasVx()) {
tracker
.setVelocity(
Vector3(
positionMessage.vx,
positionMessage.vy,
positionMessage.vz,
),
)
}
tracker.dataTick()
}
}
Expand Down
Loading