Skip to content
Closed
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
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
14 changes: 14 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()

var velocityConfig: VelocityConfig = VelocityConfig()

val vrcConfig: VRCConfig = VRCConfig()

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

/**
* Applies the velocity policy to the given [Tracker].
*
* This method determines whether the tracker—be it a physical sensor or a computed virtual device—is
* permitted to calculate and broadcast velocity data.
* It resolves the active [VelocityConfig] value, updating [Tracker.allowVelocity] accordingly.
*/
fun applyVelocityPolicy(tracker: Tracker) {
tracker.allowVelocity = velocityConfig.sendDerivedVelocity
}

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

fun writeTrackerConfig(tracker: Tracker?) {
Expand Down
10 changes: 10 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,10 @@
package dev.slimevr.config

/**
* 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 {
var sendDerivedVelocity: Boolean = false // Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message.
}
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
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 @@ -103,23 +104,38 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte

@VRServerThread
protected fun writeTrackerUpdate(localTracker: Tracker?) {
val builder = ProtobufMessages.Position.newBuilder().setTrackerId(
localTracker!!.id,
)
if (localTracker.hasPosition) {
val pos = localTracker.position
val tracker = localTracker ?: return

val builder = ProtobufMessages.Position.newBuilder()
.setTrackerId(tracker.id)

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

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

if (tracker.allowVelocity) {
val vel = tracker.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 +185,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