@@ -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