עבודה עם הידיים באמצעות ARCore ל-Jetpack XR

ARCore ל-Jetpack XR יכול לספק מידע על הידיים של המשתמש שזוהו, ומספק מידע על תנוחת הידיים והמפרקים המשויכים אליהן. אפשר להשתמש בנתוני הידיים האלה כדי לצרף ישויות ומודלים לידיים של משתמש, למשל תפריט כלים:

קבלת סשן

גישה למידע על הידיים דרך מכשיר Android XR Session. במאמר הסבר על מחזור החיים של סשן מוסבר איך לקבל את הערך Session.

הגדרת הסשן

מעקב אחר תנועות הידיים לא מופעל כברירת מחדל בסשנים ב-XR. כדי לקבל נתוני יד, צריך להגדיר את הסשן:

val newConfig = session.config.copy(
    handTracking = Config.HandTrackingMode.Enabled
)
when (val result = session.configure(newConfig)) {
    is SessionConfigureConfigurationNotSupported ->
        TODO(/* Some combinations of configurations are not valid. Handle this failure case. */)
    is SessionConfigurePermissionsNotGranted ->
        TODO(/* The required permissions in result.permissions have not been granted. */)
    is SessionConfigureSuccess -> TODO(/* Success! */)
}

אחזור נתוני היד

נתוני היד זמינים בנפרד ליד ימין וליד שמאל. משתמשים ב-state של כל יד כדי לגשת למיקומי התנוחה של כל מפרק:

Hand.left(session)?.state?.collect { handState -> // or Hand.right(session)
    // Hand state has been updated.
    // Use the state of hand joints to update an entity's position.
    renderPlanetAtHandPalm(handState)
}

למאפיינים של הידיים יש את המאפיינים הבאים:

  • isActive: האם מתבצע מעקב אחר היד.
  • handJoints: מפה של המפרקים של היד לתנוחות. תנוחות המפרקים של היד מצוינות לפי הסטנדרטים של OpenXR.

שימוש בנתוני היד באפליקציה

אפשר להשתמש במיקומים של המפרקים בידיים של המשתמש כדי לעגן אובייקטים תלת-ממדיים לידיים של המשתמש. לדוגמה, כדי לצרף מודל לכף היד הימנית:

val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return

// the down direction points in the same direction as the palm
val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up)
palmEntity.setHidden(angle > Math.toRadians(40.0))

val transformedPose =
    session.scene.perceptionSpace.transformPoseTo(
        palmPose,
        session.scene.activitySpace,
    )
val newPosition = transformedPose.translation + transformedPose.down * 0.05f
palmEntity.setPose(Pose(newPosition, transformedPose.rotation))

לחלופין, כדי לצרף מודל לקצה האצבע המורה של היד הימנית:

val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return

// the forward direction points towards the finger tip.
val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up)
indexFingerEntity.setHidden(angle > Math.toRadians(40.0))

val transformedPose =
    session.scene.perceptionSpace.transformPoseTo(
        tipPose,
        session.scene.activitySpace,
    )
val position = transformedPose.translation + transformedPose.forward * 0.03f
val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up)
indexFingerEntity.setPose(Pose(position, rotation))

זיהוי תנועות ידיים בסיסיות

זיהוי תנועות ידיים בסיסיות על סמך תנוחות המפרקים ביד. כדאי לעיין במאמר הסכמות לגבי המפרקים של היד כדי לקבוע באיזה טווח תנוחות המפרקים צריכים להיות כדי להירשם כתנוחה מסוימת.

לדוגמה, כדי לזהות צביטה עם האגודל והאצבע הבוהן, משתמשים במרחק בין שני המפרקים של קצות האצבעות:

val thumbTip = handState.handJoints[HandJointType.THUMB_TIP] ?: return false
val thumbTipPose = session.scene.perceptionSpace.transformPoseTo(thumbTip, session.scene.activitySpace)
val indexTip = handState.handJoints[HandJointType.INDEX_TIP] ?: return false
val indexTipPose = session.scene.perceptionSpace.transformPoseTo(indexTip, session.scene.activitySpace)
return Vector3.distance(thumbTipPose.translation, indexTipPose.translation) < 0.05

דוגמה לתנועה מורכבת יותר היא תנועת 'עצירה'. בתנועה הזו, כל אצבע צריכה להיות מושטחת, כלומר כל המפרקים בכל אצבע צריכים לכוון בערך לאותו כיוון:

val threshold = toRadians(angleInDegrees = 30f)
fun pointingInSameDirection(joint1: HandJointType, joint2: HandJointType): Boolean {
    val forward1 = handState.handJoints[joint1]?.forward ?: return false
    val forward2 = handState.handJoints[joint2]?.forward ?: return false
    return Vector3.angleBetween(forward1, forward2) < threshold
}
return pointingInSameDirection(HandJointType.INDEX_PROXIMAL, HandJointType.INDEX_TIP) &&
    pointingInSameDirection(HandJointType.MIDDLE_PROXIMAL, HandJointType.MIDDLE_TIP) &&
    pointingInSameDirection(HandJointType.RING_PROXIMAL, HandJointType.RING_TIP)

כשאתם מפתחים זיהוי מותאם אישית של תנועות ידיים, חשוב לזכור את הדברים הבאים:

  • יכול להיות שלמשתמשים תהיה פרשנות שונה לכל מחווה נתונה. לדוגמה, חלק מהאנשים עשויים להגדיר את התנוחה 'עצירה' כתנוחה שבה האצבעות פרושות, בעוד שאחרים עשויים למצוא שהתנוחה 'עצירה' אינטואיטיבית יותר כשהאצבעות צמודות זו לזו.
  • יכול להיות שחלק מהתנועות לא יהיו נוחות. להשתמש בתנועות אינטואיטיביות שלא מעייפות את הידיים של המשתמש.

קביעת היד המשנית של המשתמש

מערכת Android ממפה את הניווט במערכת ליד המשתמש הראשית, כפי שהמשתמש ציין בהעדפות המערכת. כדי למנוע התנגשויות עם תנועות הניווט של המערכת, כדאי להשתמש ביד השנייה לתנועות בהתאמה אישית:

val handedness = Hand.getHandedness(activity.contentResolver)
val secondaryHand = if (handedness == Hand.Handedness.LEFT) Hand.right(session) else Hand.left(session)
val handState = secondaryHand?.state ?: return
detectGesture(handState)