Skip to content

Commit 7f7da7e

Browse files
committed
[Spine Yaw Compensation] Add small yaw rotations to keep spine trackers aligned
1 parent 5c5636a commit 7f7da7e

File tree

12 files changed

+230
-2
lines changed

12 files changed

+230
-2
lines changed

gui/public/i18n/en/translation.ftl

+7
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ settings-general-tracker_mechanics-use_mag_on_all_trackers-description =
373373
Uses magnetometer on all trackers that have a compatible firmware for it, reducing drift in stable magnetic environments.
374374
Can be disabled per tracker in the tracker's settings. <b>Please don't shutdown any of the trackers while toggling this!</b>
375375
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = Use magnetometer on trackers
376+
settings-general-tracker_mechanics-spine_yaw_correction = Keep Spine Aligned
377+
settings-general-tracker_mechanics-spine_yaw_correction-description =
378+
Prevents the spine trackers from drifting, by nudging them to stay aligned to the headset.
379+
Players are usually standing facing forwards or walking forwards, where the spine trackers are aligned to the yaw of the headset. However due to yaw drift, the spine trackers will eventually rotate to the side, requiring a reset. This feature adds a small centering rotation to compensate for the yaw drift. (For now, this compensation only activates when your trackers are pointing "up", e.g. when you're standing or sitting straight up.)
380+
settings-general-tracker_mechanics-spine_yaw_correction-enabled-label = Enabled
381+
settings-general-tracker_mechanics-spine_yaw_correction-amount-label = Amount
382+
settings-general-tracker_mechanics-spine_yaw_correction-amount-description = The correction amount should be approximately the maximum yaw drift of your gyroscope (0.3 degrees/sec is a good default).
376383
377384
## FK/Tracking settings
378385
settings-general-fk_settings = Tracking settings

gui/src/components/settings/pages/GeneralSettings.tsx

+72
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
SettingsResponseT,
1717
SteamVRTrackersSettingT,
1818
TapDetectionSettingsT,
19+
YawCorrectionSettingsT,
1920
} from 'solarxr-protocol';
2021
import { useConfig } from '@/hooks/config';
2122
import { useWebsocketAPI } from '@/hooks/websocket-api';
@@ -101,6 +102,10 @@ interface SettingsForm {
101102
saveMountingReset: boolean;
102103
resetHmdPitch: boolean;
103104
};
105+
yawCorrectionSettings: {
106+
enabled: boolean;
107+
amountInDegPerSec: number;
108+
};
104109
}
105110

106111
const defaultValues: SettingsForm = {
@@ -165,6 +170,10 @@ const defaultValues: SettingsForm = {
165170
saveMountingReset: false,
166171
resetHmdPitch: false,
167172
},
173+
yawCorrectionSettings: {
174+
enabled: false,
175+
amountInDegPerSec: 0.0,
176+
},
168177
};
169178

170179
export function GeneralSettings() {
@@ -184,6 +193,10 @@ export function GeneralSettings() {
184193
unitDisplay: 'narrow',
185194
maximumFractionDigits: 2,
186195
});
196+
const degreeFormat = new Intl.NumberFormat(currentLocales, {
197+
style: 'unit',
198+
unit: 'degree',
199+
});
187200

188201
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
189202
const { reset, control, watch, handleSubmit, getValues, setValue } =
@@ -291,6 +304,12 @@ export function GeneralSettings() {
291304
driftCompensation.maxResets = values.driftCompensation.maxResets;
292305
settings.driftCompensation = driftCompensation;
293306

307+
const yawCorrectionSettings = new YawCorrectionSettingsT();
308+
yawCorrectionSettings.enabled = values.yawCorrectionSettings.enabled;
309+
yawCorrectionSettings.amountInDegPerSec =
310+
values.yawCorrectionSettings.amountInDegPerSec;
311+
settings.yawCorrectionSettings = yawCorrectionSettings;
312+
294313
if (values.resetsSettings) {
295314
const resetsSettings = new ResetsSettingsT();
296315
resetsSettings.resetMountingFeet =
@@ -414,6 +433,10 @@ export function GeneralSettings() {
414433
formData.resetsSettings = settings.resetsSettings;
415434
}
416435

436+
if (settings.yawCorrectionSettings) {
437+
formData.yawCorrectionSettings = settings.yawCorrectionSettings;
438+
}
439+
417440
reset({ ...getValues(), ...formData });
418441
});
419442

@@ -813,6 +836,55 @@ export function GeneralSettings() {
813836
settingType="general"
814837
id="mechanics-magnetometer"
815838
/>
839+
<div className="flex flex-col pt-4 pb-4"></div>
840+
<Typography bold>
841+
{l10n.getString(
842+
'settings-general-tracker_mechanics-spine_yaw_correction'
843+
)}
844+
</Typography>
845+
<div className="flex flex-col pt-2 pb-4">
846+
{l10n
847+
.getString(
848+
'settings-general-tracker_mechanics-spine_yaw_correction-description'
849+
)
850+
.split('\n')
851+
.map((line, i) => (
852+
<Typography color="secondary" key={i}>
853+
{line}
854+
</Typography>
855+
))}
856+
</div>
857+
<div className="grid sm:grid-cols-1 gap-3 pb-4">
858+
<CheckBox
859+
variant="toggle"
860+
outlined
861+
control={control}
862+
name="yawCorrectionSettings.enabled"
863+
label={l10n.getString(
864+
'settings-general-tracker_mechanics-spine_yaw_correction-enabled-label'
865+
)}
866+
/>
867+
<div>
868+
<Typography bold>
869+
{l10n.getString(
870+
'settings-general-tracker_mechanics-spine_yaw_correction-amount-label'
871+
)}
872+
</Typography>
873+
<Typography color="secondary">
874+
{l10n.getString(
875+
'settings-general-tracker_mechanics-spine_yaw_correction-amount-description'
876+
)}
877+
</Typography>
878+
<NumberSelector
879+
control={control}
880+
name="yawCorrectionSettings.amountInDegPerSec"
881+
valueLabelFormat={(value) => degreeFormat.format(value)}
882+
min={0.0}
883+
max={2.0}
884+
step={0.05}
885+
/>
886+
</div>
887+
</div>
816888
</>
817889
</SettingsPagePaneLayout>
818890
<SettingsPagePaneLayout

server/core/src/main/java/dev/slimevr/config/VRConfig.kt

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class VRConfig {
4040

4141
val resetsConfig: ResetsConfig = ResetsConfig()
4242

43+
val yawCorrectionConfig = YawCorrectionConfig()
44+
4345
@JsonDeserialize(using = TrackerConfigMapDeserializer::class)
4446
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class)
4547
private val trackers: MutableMap<String, TrackerConfig> = HashMap()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.slimevr.config
2+
3+
class YawCorrectionConfig {
4+
5+
var enabled = false
6+
7+
var amountInDegPerSec = 0.3f
8+
}

server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java

+17
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,18 @@ public static int createArmsResetModeSettings(
349349
);
350350
}
351351

352+
public static int createYawCorrectionSettings(
353+
FlatBufferBuilder fbb,
354+
YawCorrectionConfig yawCorrectionConfig
355+
) {
356+
return YawCorrectionSettings
357+
.createYawCorrectionSettings(
358+
fbb,
359+
yawCorrectionConfig.getEnabled(),
360+
yawCorrectionConfig.getAmountInDegPerSec()
361+
);
362+
}
363+
352364
public static int createSettingsResponse(FlatBufferBuilder fbb, VRServer server) {
353365
ISteamVRBridge bridge = server.getVRBridge(ISteamVRBridge.class);
354366

@@ -401,6 +413,11 @@ public static int createSettingsResponse(FlatBufferBuilder fbb, VRServer server)
401413
.createArmsResetModeSettings(
402414
fbb,
403415
server.configManager.getVrConfig().getResetsConfig()
416+
),
417+
RPCSettingsBuilder
418+
.createYawCorrectionSettings(
419+
fbb,
420+
server.configManager.getVrConfig().getYawCorrectionConfig()
404421
)
405422
);
406423
}

server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,15 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
345345
resetsConfig.updateTrackersResetsSettings()
346346
}
347347

348+
if (req.yawCorrectionSettings() != null) {
349+
val yawCorrectionConfig = api.server.configManager
350+
.vrConfig
351+
.yawCorrectionConfig
352+
yawCorrectionConfig.enabled = req.yawCorrectionSettings().enabled()
353+
yawCorrectionConfig.amountInDegPerSec = req.yawCorrectionSettings().amountInDegPerSec()
354+
api.server.humanPoseManager.setYawCorrection(yawCorrectionConfig)
355+
}
356+
348357
api.server.configManager.saveConfig()
349358
}
350359

@@ -361,7 +370,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
361370
val settings = SettingsResponse
362371
.createSettingsResponse(
363372
fbb,
364-
RPCSettingsBuilder.createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0,
373+
RPCSettingsBuilder.createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
365374
)
366375
val outbound =
367376
rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings)

server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.jme3.math.FastMath
44
import dev.slimevr.VRServer
55
import dev.slimevr.VRServer.Companion.getNextLocalTrackerId
66
import dev.slimevr.config.ConfigManager
7+
import dev.slimevr.config.YawCorrectionConfig
78
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
89
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
910
import dev.slimevr.tracking.processor.config.SkeletonConfigToggles
@@ -286,6 +287,7 @@ class HumanPoseManager(val server: VRServer?) {
286287

287288
fun loadFromConfig(configManager: ConfigManager) {
288289
skeletonConfigManager.loadFromConfig(configManager)
290+
skeleton.setYawCorrectionConfig(configManager.vrConfig.yawCorrectionConfig)
289291
}
290292

291293
@VRServerThread
@@ -663,6 +665,10 @@ class HumanPoseManager(val server: VRServer?) {
663665
}
664666
}
665667

668+
fun setYawCorrection(yawCorrectionConfig: YawCorrectionConfig) {
669+
skeleton.setYawCorrectionConfig(yawCorrectionConfig)
670+
}
671+
666672
fun setLegTweaksStateTemp(
667673
skatingCorrection: Boolean,
668674
floorClip: Boolean,

server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.slimevr.tracking.processor.skeleton
22

33
import com.jme3.math.FastMath
44
import dev.slimevr.VRServer
5+
import dev.slimevr.config.YawCorrectionConfig
56
import dev.slimevr.tracking.processor.Bone
67
import dev.slimevr.tracking.processor.BoneType
78
import dev.slimevr.tracking.processor.HumanPoseManager
@@ -141,12 +142,14 @@ class HumanSkeleton(
141142

142143
// Others
143144
private var pauseTracking = false // Pauses skeleton tracking if true, resumes skeleton tracking if false
145+
var yawCorrectionInDegPerSec = 0.0f
144146

145147
// Modules
146148
var legTweaks = LegTweaks(this)
147149
var tapDetectionManager = TapDetectionManager(this)
148150
var viveEmulation = ViveEmulation(this)
149151
var localizer = Localizer(this)
152+
var spineYawCorrection = SpineYawCorrection(this, YawCorrectionConfig())
150153

151154
// Constructors
152155
init {
@@ -360,6 +363,9 @@ class HumanSkeleton(
360363
fun updatePose() {
361364
tapDetectionManager.update()
362365

366+
// Need to update tracker rotations before we calculate the new skeleton poses
367+
spineYawCorrection.updateTrackers()
368+
363369
updateTransforms()
364370
updateBones()
365371
updateComputedTrackers()
@@ -1239,6 +1245,10 @@ class HumanSkeleton(
12391245
return newState
12401246
}
12411247

1248+
fun setYawCorrectionConfig(config: YawCorrectionConfig) {
1249+
spineYawCorrection = SpineYawCorrection(this, config)
1250+
}
1251+
12421252
companion object {
12431253
val FORWARD_QUATERNION = EulerAngles(
12441254
EulerOrder.YZX,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dev.slimevr.tracking.processor.skeleton
2+
3+
import com.jme3.math.FastMath
4+
import dev.slimevr.VRServer
5+
import dev.slimevr.config.YawCorrectionConfig
6+
import dev.slimevr.tracking.trackers.Tracker
7+
import io.github.axisangles.ktmath.EulerOrder
8+
import io.github.axisangles.ktmath.Quaternion
9+
import io.github.axisangles.ktmath.Vector3.Companion.POS_Y
10+
import kotlin.math.*
11+
12+
class SpineYawCorrection(
13+
val skeleton: HumanSkeleton,
14+
private val yawCorrectionConfig: YawCorrectionConfig,
15+
) {
16+
17+
private val upperBodyTrackers = filterImuTrackers(
18+
skeleton.headTracker,
19+
skeleton.neckTracker,
20+
skeleton.upperChestTracker,
21+
skeleton.chestTracker,
22+
skeleton.waistTracker,
23+
skeleton.hipTracker,
24+
)
25+
26+
fun updateTrackers() {
27+
if (!yawCorrectionConfig.enabled) {
28+
return
29+
}
30+
31+
updateTrackers(upperBodyTrackers)
32+
}
33+
34+
private fun updateTrackers(trackers: List<Tracker>) {
35+
for ((parentTracker, tracker) in trackers.zipWithNext()) {
36+
updateTracker(tracker, parentTracker)
37+
}
38+
}
39+
40+
private fun updateTracker(tracker: Tracker, parentTracker: Tracker) {
41+
val trackerRot = tracker.getRotation()
42+
val parentTrackerRot = parentTracker.getRotation()
43+
44+
// For now, we only handle trackers which are relatively "upright", i.e. in the
45+
// standing position, or sitting position for trackers that are high up on the
46+
// spine. When someone is lying down, they could be curled up such that the yaws
47+
// don't necessarily align.
48+
val maxDeviationInRad = 30.0f * FastMath.DEG_TO_RAD
49+
if (!isTrackerPointingUp(trackerRot, maxDeviationInRad) ||
50+
!isTrackerPointingUp(parentTrackerRot, maxDeviationInRad)
51+
) {
52+
return
53+
}
54+
55+
val deltaRot = trackerRot * parentTrackerRot.inv()
56+
val deltaYawInRad = deltaRot.toEulerAngles(EulerOrder.YZX).y
57+
58+
// Amount of yaw should be roughly the maximum yaw bias of the gyroscope. If it
59+
// is too small, the gyroscope will overpower the correction and the skeleton
60+
// will drift. If it is too big, the player will notice that the skeleton is
61+
// rotating when the player doesn't face forward for a long time.
62+
val adjustYawInRad = -sign(deltaYawInRad) * (yawCorrectionConfig.amountInDegPerSec * FastMath.DEG_TO_RAD) * VRServer.instance.fpsTimer.timePerFrame
63+
64+
// Adjust the tracker's yaw towards the parent tracker's yaw
65+
tracker.resetsHandler.spineYawCorrectionInRad += adjustYawInRad
66+
}
67+
68+
private fun isTrackerPointingUp(trackerRot: Quaternion, maxDeviationInRad: Float): Boolean {
69+
val trackerUp = trackerRot.sandwich(POS_Y)
70+
return trackerUp.angleTo(POS_Y) < maxDeviationInRad
71+
}
72+
73+
companion object {
74+
private fun filterImuTrackers(vararg trackers: Tracker?): List<Tracker> = trackers.filterNotNull().filter { it.isImu() }.toList()
75+
}
76+
}

server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt

+4
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ class Tracker @JvmOverloads constructor(
333333
rot = resetsHandler.getReferenceAdjustedDriftRotationFrom(rot)
334334
}
335335

336+
rot = resetsHandler.applySpineYawCorrection(rot)
337+
336338
return rot
337339
}
338340

@@ -365,6 +367,8 @@ class Tracker @JvmOverloads constructor(
365367
rot = resetsHandler.getIdentityAdjustedDriftRotationFrom(rot)
366368
}
367369

370+
rot = resetsHandler.applySpineYawCorrection(rot)
371+
368372
return rot
369373
}
370374

0 commit comments

Comments
 (0)