Skip to content

Commit a853b98

Browse files
committed
added a system to figure out the elbows from the controllers destinations
1 parent 183c3c7 commit a853b98

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

Basis/Packages/com.basis.framework/Avatar/BasisAvatarIKStageCalibration.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ private static void ClassifyAndAssignTrackersFromTPose()
216216
ConstellationDebug.ArmReach = armReach;
217217

218218
BoneRolePrior[] priors = BuildPriors(armReach);
219+
220+
// Re-center the elbow (lower-arm) priors on the line between the known hand
221+
// controller and the estimated shoulder. The hand controllers already carry a
222+
// pinned role, so their pose is reliable even when the player can't hold a clean
223+
// T-pose; anchoring the elbow region to the hand→shoulder midpoint keeps a
224+
// drooped or slightly-forward forearm tracker inside its acceptance region
225+
// instead of falling outside the static T-pose circle. Runs before the snapshot
226+
// capture so the editor visualizer and the in-VR calibration spheres pick up the
227+
// moved region for free.
228+
ApplyElbowMidpointPriors(priors, bodyOrigin, bodyRotInv, eyeHeight);
229+
219230
CapturePriorSnapshots(priors);
220231

221232
// Honor per-role calibration toggles from the body-tracking settings UI.
@@ -716,6 +727,121 @@ private static BoneRolePrior[] BuildPriors(float armReach)
716727
};
717728
}
718729

730+
/// <summary>
731+
/// Overrides the LeftLowerArm / RightLowerArm prior centers with the midpoint
732+
/// between that side's hand controller and shoulder. Hand controllers keep a pinned
733+
/// role, so their pose is trustworthy even in a sloppy calibration stance — the elbow
734+
/// sits roughly halfway down the arm, so the hand→shoulder midpoint tracks the real
735+
/// forearm far better than a fixed T-pose point. No-ops per side when that hand isn't
736+
/// present (hand-tracking off, tracker-only hand, or a stale poll): the static
737+
/// <see cref="BuildPriors"/> center stands in.
738+
/// </summary>
739+
private static void ApplyElbowMidpointPriors(BoneRolePrior[] priors, Vector3 bodyOrigin, Quaternion bodyRotInv, float eyeHeight)
740+
{
741+
ApplyElbowMidpointForSide(priors, BasisBoneTrackedRole.LeftHand, BasisBoneTrackedRole.LeftShoulder, BasisBoneTrackedRole.LeftLowerArm, sideSign: -1, bodyOrigin, bodyRotInv, eyeHeight);
742+
ApplyElbowMidpointForSide(priors, BasisBoneTrackedRole.RightHand, BasisBoneTrackedRole.RightShoulder, BasisBoneTrackedRole.RightLowerArm, sideSign: +1, bodyOrigin, bodyRotInv, eyeHeight);
743+
}
744+
745+
private static void ApplyElbowMidpointForSide(
746+
BoneRolePrior[] priors,
747+
BasisBoneTrackedRole handRole,
748+
BasisBoneTrackedRole shoulderRole,
749+
BasisBoneTrackedRole lowerArmRole,
750+
int sideSign,
751+
Vector3 bodyOrigin, Quaternion bodyRotInv, float eyeHeight)
752+
{
753+
int elbowIdx = FindPriorIndex(priors, lowerArmRole);
754+
if (elbowIdx < 0) return; // lower-arm role isn't in the prior set
755+
756+
if (!TryGetHandBodyLocalRatios(handRole, bodyOrigin, bodyRotInv, eyeHeight, out float handHeight, out float handLateral))
757+
{
758+
return; // no usable hand pose this side — keep the static T-pose prior
759+
}
760+
761+
// Shoulder anchor: reuse the shoulder prior's expected position so the elbow
762+
// stays consistent with the shoulder region. (Shoulders are always present in
763+
// the freshly-built prior list — the calibration-toggle filter runs later — so
764+
// the fallback is purely defensive.)
765+
float shoulderHeight, shoulderLateral;
766+
int shoulderIdx = FindPriorIndex(priors, shoulderRole);
767+
if (shoulderIdx >= 0)
768+
{
769+
shoulderHeight = priors[shoulderIdx].ExpectedHeightRatio;
770+
shoulderLateral = priors[shoulderIdx].ExpectedLateralRatio;
771+
}
772+
else
773+
{
774+
shoulderHeight = priors[elbowIdx].ExpectedHeightRatio;
775+
shoulderLateral = priors[elbowIdx].ExpectedLateralRatio;
776+
}
777+
778+
float t = ConstellationElbowShoulderBlend;
779+
float elbowHeight = Mathf.Lerp(shoulderHeight, handHeight, t);
780+
float elbowLateral = Mathf.Lerp(shoulderLateral, handLateral, t);
781+
782+
// Keep the elbow region on its own side of the midline. Pulling the center too
783+
// far inward (hands held across the body) would let the 3σ band cross x=0 and
784+
// pick up the opposite arm's tracker; clamp the magnitude so the band's inner
785+
// edge stays at/beyond the centerline, matching BuildPriors' "never cross the
786+
// midline" intent.
787+
float latSigma = priors[elbowIdx].LateralSigma;
788+
float minMag = ConstellationElbowMidlineSigmaGuard * latSigma;
789+
float clampedLateral = sideSign < 0 ? Mathf.Min(elbowLateral, -minMag) : Mathf.Max(elbowLateral, minMag);
790+
791+
priors[elbowIdx] = new BoneRolePrior(
792+
lowerArmRole,
793+
elbowHeight,
794+
clampedLateral,
795+
priors[elbowIdx].HeightSigma,
796+
latSigma);
797+
798+
BasisDebug.Log($"FBIK constellation: {lowerArmRole} prior re-centered on hand→shoulder midpoint (h={elbowHeight:F2}, lat={clampedLateral:F2}; hand h={handHeight:F2}, lat={handLateral:F2})", BasisDebug.LogTag.Input);
799+
}
800+
801+
/// <summary>
802+
/// Finds the device currently bound to <paramref name="handRole"/> and returns its
803+
/// body-local height/lateral ratios in the same playspace frame the classifier uses
804+
/// (UnscaledDeviceCoord, normalized to eye height). Returns false when no such device
805+
/// exists or it polled at the world origin (a pose it never actually wrote).
806+
/// </summary>
807+
private static bool TryGetHandBodyLocalRatios(BasisBoneTrackedRole handRole, Vector3 bodyOrigin, Quaternion bodyRotInv, float eyeHeight, out float heightRatio, out float lateralRatio)
808+
{
809+
heightRatio = 0f;
810+
lateralRatio = 0f;
811+
812+
BasisObservableList<BasisInput> devices = BasisDeviceManagement.Instance.AllInputDevices;
813+
int count = devices.Count;
814+
for (int i = 0; i < count; i++)
815+
{
816+
BasisInput input = devices[i];
817+
if (input == null) continue;
818+
if (!input.TryGetRole(out BasisBoneTrackedRole role) || role != handRole) continue;
819+
820+
// Same fresh-poll discipline as the HMD and free-tracker reads in this pass.
821+
input.LatePollData();
822+
Vector3 unscaledPos = input.UnscaledDeviceCoord.position;
823+
if (unscaledPos.sqrMagnitude < ConstellationNearOriginEpsilonSqr)
824+
{
825+
return false; // hand never wrote a real pose — don't anchor the elbow to it
826+
}
827+
828+
Vector3 local = bodyRotInv * (unscaledPos - bodyOrigin);
829+
heightRatio = local.y / eyeHeight;
830+
lateralRatio = local.x / eyeHeight;
831+
return true;
832+
}
833+
return false;
834+
}
835+
836+
private static int FindPriorIndex(BoneRolePrior[] priors, BasisBoneTrackedRole role)
837+
{
838+
for (int i = 0; i < priors.Length; i++)
839+
{
840+
if (priors[i].Role == role) return i;
841+
}
842+
return -1;
843+
}
844+
719845
// Returns true when dependsOn is already taken, or isn't in the prior list at all
720846
// (the toggle disabled it, so there's nothing for the dependent role to wait on).
721847
private static bool IsRolePreconditionMet(BoneRolePrior[] priors, bool[] roleUsed, BasisBoneTrackedRole dependsOn)
@@ -750,6 +876,14 @@ private static float ScoreSampleAgainstRole(TrackerSample sample, BoneRolePrior
750876
// when no arm-height tracker is present to measure the player's own reach.
751877
private const float ConstellationDefaultArmReachRatio = 0.55f;
752878
private const float ConstellationToeForwardEpsilon = 0.02f;
879+
// Where the elbow prior sits along the hand→shoulder line. 0.5 = true midpoint (the
880+
// elbow sits ~halfway down a roughly-straight arm); raise toward 1 to bias the
881+
// region toward the hand, lower toward 0 to bias it toward the shoulder.
882+
private const float ConstellationElbowShoulderBlend = 0.5f;
883+
// Floor on the re-centered elbow's lateral magnitude, in units of its own lateral
884+
// sigma, so the region's inner edge can't cross the body midline onto the other arm.
885+
// 3σ matches the accept threshold.
886+
private const float ConstellationElbowMidlineSigmaGuard = 3.0f;
753887
/// <summary>
754888
/// gets a roles dictionary with the roles and transforms
755889
/// </summary>

0 commit comments

Comments
 (0)