Skip to content

Commit f114427

Browse files
committed
Add positional constraint code
1 parent c649b9a commit f114427

File tree

9 files changed

+472
-2
lines changed

9 files changed

+472
-2
lines changed

gui/public/i18n/en/translation.ftl

+3
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,9 @@ settings-general-fk_settings-enforce_joint_constraints-enforce_constraints = Enf
463463
settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description = Prevents joints from rotating past their limit
464464
settings-general-fk_settings-enforce_joint_constraints-correct_constraints = Correct with constraints
465465
settings-general-fk_settings-enforce_joint_constraints-correct_constraints-description = Correct joint rotations when they push past their limit
466+
settings-general-fk_settings-ik = Position data
467+
settings-general-fk_settings-ik-use_position = Use Position data
468+
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.
466469
settings-general-fk_settings-arm_fk = Arm tracking
467470
settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the headset (HMD) even if positional hand data is available.
468471
settings-general-fk_settings-arm_fk-force_arms = Force arms from HMD

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

+22
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,28 @@ export function GeneralSettings() {
10651065
/>
10661066
</div>
10671067

1068+
<div className="flex flex-col pt-2 pb-3">
1069+
<Typography bold>
1070+
{l10n.getString('settings-general-fk_settings-ik')}
1071+
</Typography>
1072+
<Typography color="secondary">
1073+
{l10n.getString(
1074+
'settings-general-fk_settings-ik-use_position-description'
1075+
)}
1076+
</Typography>
1077+
</div>
1078+
<div className="grid sm:grid-cols-1 pb-3">
1079+
<CheckBox
1080+
variant="toggle"
1081+
outlined
1082+
control={control}
1083+
name="toggles.usePosition"
1084+
label={l10n.getString(
1085+
'settings-general-fk_settings-ik-use_position'
1086+
)}
1087+
/>
1088+
</div>
1089+
10681090
{config?.debug && (
10691091
<>
10701092
<div className="flex flex-col pt-2 pb-3">

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public static int createModelSettings(
189189
humanPoseManager.getToggle(SkeletonConfigToggles.TOE_SNAP),
190190
humanPoseManager.getToggle(SkeletonConfigToggles.FOOT_PLANT),
191191
humanPoseManager.getToggle(SkeletonConfigToggles.SELF_LOCALIZATION),
192-
false,
192+
humanPoseManager.getToggle(SkeletonConfigToggles.USE_POSITION),
193193
humanPoseManager.getToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS),
194194
humanPoseManager.getToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS)
195195
);

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

+1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
258258
hpm.setToggle(SkeletonConfigToggles.TOE_SNAP, toggles.toeSnap())
259259
hpm.setToggle(SkeletonConfigToggles.FOOT_PLANT, toggles.footPlant())
260260
hpm.setToggle(SkeletonConfigToggles.SELF_LOCALIZATION, toggles.selfLocalization())
261+
hpm.setToggle(SkeletonConfigToggles.USE_POSITION, toggles.usePosition())
261262
hpm.setToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS, toggles.enforceConstraints())
262263
hpm.setToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS, toggles.correctConstraints())
263264
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,11 @@ class HumanPoseManager(val server: VRServer?) {
639639
skeleton.setLegTweaksEnabled(value)
640640
}
641641

642+
@VRServerThread
643+
fun setIKSolverEnabled(value: Boolean) {
644+
skeleton.setIKSolverEnabled(value)
645+
}
646+
642647
@VRServerThread
643648
fun setFloorClipEnabled(value: Boolean) {
644649
skeleton.setFloorclipEnabled(value)

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ class HumanSkeleton(
213213
var legTweaks = LegTweaks(this)
214214
var tapDetectionManager = TapDetectionManager(this)
215215
var localizer = Localizer(this)
216+
var ikSolver = IKSolver(headBone)
216217

217218
// Constructors
218219
init {
@@ -450,6 +451,9 @@ class HumanSkeleton(
450451
// Update tap detection's trackers
451452
tapDetectionManager.updateConfig(trackers)
452453

454+
// Rebuild Ik Solver
455+
ikSolver.buildChains(trackers)
456+
453457
// Update bones tracker field
454458
refreshBoneTracker()
455459
}
@@ -1207,7 +1211,7 @@ class HumanSkeleton(
12071211

12081212
SkeletonConfigToggles.SELF_LOCALIZATION -> localizer.setEnabled(newValue)
12091213

1210-
SkeletonConfigToggles.USE_POSITION -> newValue
1214+
SkeletonConfigToggles.USE_POSITION -> ikSolver.enabled = newValue
12111215

12121216
SkeletonConfigToggles.ENFORCE_CONSTRAINTS -> enforceConstraints = newValue
12131217

@@ -1573,6 +1577,7 @@ class HumanSkeleton(
15731577
}
15741578
legTweaks.resetBuffer()
15751579
localizer.reset()
1580+
ikSolver.resetOffsets()
15761581
LogManager.info("[HumanSkeleton] Reset: full ($resetSourceName)")
15771582
}
15781583

@@ -1706,6 +1711,14 @@ class HumanSkeleton(
17061711
legTweaks.enabled = value
17071712
}
17081713

1714+
/**
1715+
* enable/disable IK solver (for Autobone)
1716+
*/
1717+
@VRServerThread
1718+
fun setIKSolverEnabled(value: Boolean) {
1719+
ikSolver.enabled = value
1720+
}
1721+
17091722
@VRServerThread
17101723
fun setFloorclipEnabled(value: Boolean) {
17111724
humanPoseManager.setToggle(SkeletonConfigToggles.FLOOR_CLIP, value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package dev.slimevr.tracking.processor.skeleton
2+
3+
import dev.slimevr.tracking.processor.Bone
4+
import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType
5+
import dev.slimevr.tracking.trackers.Tracker
6+
import io.github.axisangles.ktmath.Quaternion
7+
import io.github.axisangles.ktmath.Vector3
8+
import kotlin.math.*
9+
10+
/*
11+
* This class implements a chain of Bones
12+
*/
13+
14+
class IKChain(
15+
val bones: MutableList<Bone>,
16+
var parent: IKChain?,
17+
val level: Int,
18+
val baseConstraint: Tracker?,
19+
val tailConstraint: Tracker?,
20+
) {
21+
// State variables
22+
private val computedBasePosition = baseConstraint?.let { IKConstraint(it) }
23+
private val computedTailPosition = tailConstraint?.let { IKConstraint(it) }
24+
var children = mutableListOf<IKChain>()
25+
var target = Vector3.NULL
26+
var distToTargetSqr = Float.POSITIVE_INFINITY
27+
private var rotations = getRotationsList()
28+
29+
private fun getRotationsList(): MutableList<Quaternion> {
30+
val rotList = mutableListOf<Quaternion>()
31+
for (b in bones) {
32+
rotList.add(b.getGlobalRotation())
33+
}
34+
35+
return rotList
36+
}
37+
38+
/*
39+
* Populate the non-static rotations with the solve angle from the last iteration
40+
*/
41+
private fun prepBones() {
42+
for (i in 0..<bones.size) {
43+
if (bones[i].rotationConstraint.constraintType != ConstraintType.COMPLETE) {
44+
bones[i].setRotationRaw(rotations[i])
45+
}
46+
}
47+
}
48+
49+
fun backwardsCCDIK() {
50+
target = computedTailPosition?.getPosition() ?: getChildTargetAvg()
51+
var offset = Vector3.NULL
52+
53+
for (i in bones.size - 1 downTo 0) {
54+
val currentBone = bones[i]
55+
56+
// Get the local position of the end effector and the target relative to the current node
57+
val endEffectorLocal = ((getEndEffectorsAvg() - offset) - currentBone.getPosition()).unit()
58+
val targetLocal = ((target - offset) - currentBone.getPosition()).unit()
59+
60+
// Compute the axis of rotation and angle for this bone
61+
var scalar = IKSolver.DAMPENING_FACTOR * if (currentBone.rotationConstraint.hasTrackerRotation) IKSolver.STATIC_DAMPENING else 1f
62+
scalar *= ((bones.size - i).toFloat() / bones.size).pow(IKSolver.ANNEALING_EXPONENT)
63+
val adjustment = Quaternion.fromTo(endEffectorLocal, targetLocal).pow(scalar).unit()
64+
65+
val rotation = currentBone.getGlobalRotation()
66+
var correctedRot = (adjustment * rotation).unit()
67+
68+
// Bones that are not supposed to be modified should tend towards their origin
69+
if (!currentBone.rotationConstraint.allowModifications) {
70+
correctedRot = correctedRot.interpR(currentBone.rotationConstraint.initialRotation, IKSolver.CORRECTION_FACTOR)
71+
}
72+
rotations[i] = setBoneRotation(currentBone, correctedRot)
73+
74+
if (currentBone.rotationConstraint.hasTrackerRotation) {
75+
offset += rotations[i].sandwich(Vector3.NEG_Y) * currentBone.length
76+
}
77+
}
78+
}
79+
80+
private fun getEndEffectorsAvg(): Vector3 {
81+
if (children.size < 1 || computedTailPosition != null) return bones.last().getTailPosition()
82+
83+
var sum = Vector3.NULL
84+
for (c in children) {
85+
sum += c.getEndEffectorsAvg()
86+
}
87+
88+
return sum / children.size.toFloat()
89+
}
90+
91+
private fun getChildTargetAvg(): Vector3 {
92+
if (computedTailPosition != null) return computedTailPosition.getPosition()
93+
94+
var sum = Vector3.NULL
95+
for (c in children) {
96+
sum += c.getChildTargetAvg()
97+
}
98+
99+
return sum / children.size.toFloat()
100+
}
101+
102+
/**
103+
* Resets the chain to its default state
104+
*/
105+
fun resetChain() {
106+
distToTargetSqr = Float.POSITIVE_INFINITY
107+
108+
for (b in bones) {
109+
b.rotationConstraint.initialRotation = b.getGlobalRotation()
110+
}
111+
prepBones()
112+
113+
for (child in children) {
114+
child.resetChain()
115+
}
116+
}
117+
118+
fun resetTrackerOffsets() {
119+
computedTailPosition?.reset(bones.last().getTailPosition())
120+
computedBasePosition?.reset(bones.first().getPosition())
121+
}
122+
123+
/**
124+
* Updates the distance to target and other fields
125+
* Call on the root chain
126+
*/
127+
fun computeTargetDistance() {
128+
distToTargetSqr = if (computedTailPosition != null) {
129+
(bones.last().getTailPosition() - computedTailPosition.getPosition()).lenSq()
130+
} else {
131+
0.0f
132+
}
133+
134+
for (chain in children) {
135+
chain.computeTargetDistance()
136+
}
137+
}
138+
139+
/**
140+
* Sets a bones rotation after constraining the rotation
141+
* to the bone's rotational constraint
142+
* returns the constrained rotation
143+
*/
144+
private fun setBoneRotation(bone: Bone, rotation: Quaternion): Quaternion {
145+
// Constrain relative to the parent
146+
val newRotation = if (bone.rotationConstraint.constraintType == ConstraintType.COMPLETE) {
147+
bone.rotationConstraint.applyConstraint(rotation, bone)
148+
} else if (!bone.rotationConstraint.hasTrackerRotation) {
149+
bone.rotationConstraint.applyConstraint(rotation, bone)
150+
} else {
151+
bone.rotationConstraint.constrainToInitialRotation(rotation)
152+
}
153+
154+
bone.setRotationRaw(newRotation)
155+
bone.update()
156+
157+
return newRotation
158+
}
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package dev.slimevr.tracking.processor.skeleton
2+
3+
import dev.slimevr.tracking.trackers.Tracker
4+
import io.github.axisangles.ktmath.Quaternion
5+
import io.github.axisangles.ktmath.Vector3
6+
import solarxr_protocol.datatypes.BodyPart
7+
8+
class IKConstraint(val tracker: Tracker) {
9+
private var offset = Vector3.NULL
10+
private var rotationOffset = Quaternion.IDENTITY
11+
12+
fun getPosition(): Vector3 =
13+
tracker.position + (tracker.getRotation() * rotationOffset).sandwich(offset)
14+
15+
fun reset(nodePosition: Vector3) {
16+
val bodyPartsToSkip = setOf(BodyPart.LEFT_HAND, BodyPart.RIGHT_HAND)
17+
18+
rotationOffset = tracker.getRotation().inv()
19+
if (tracker.trackerPosition?.bodyPart in bodyPartsToSkip) return
20+
offset = nodePosition - tracker.position
21+
}
22+
}

0 commit comments

Comments
 (0)