Trabalhar com as mãos usando o ARCore para Jetpack XR

O ARCore para Jetpack XR pode fornecer informações sobre as mãos detectadas do usuário e informações de pose para as mãos e as articulações associadas. Esses dados de mão podem ser usados para anexar entidades e modelos às mãos de um usuário, por exemplo, um menu de ferramentas:

Conseguir uma sessão

Acessar informações das mãos por meio de um Session do Android XR. Consulte Entender o ciclo de vida de uma sessão para receber uma Session.

Configurar a sessão

O rastreamento de mãos não está ativado por padrão nas sessões de XR. Para receber dados de mãos, configure a sessão:

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! */)
}

Extrair dados da mão

Os dados de mãos estão disponíveis separadamente para a mão esquerda e a direita. Use o state de cada mão para acessar as posições de pose de cada articulação:

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)
}

As mãos têm as seguintes propriedades:

  • isActive: se a mão está sendo rastreada ou não.
  • handJoints: um mapa das articulações das mãos para poses. As poses das articulações das mãos são especificadas pelos padrões do OpenXR.

Usar dados de mãos no app

As posições das articulações das mãos de um usuário podem ser usadas para fixar objetos 3D nas mãos de um usuário, por exemplo, para anexar um modelo à palma da mão esquerda:

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

Ou para anexar um modelo à ponta do dedo indicador da mão direita:

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

Detectar gestos básicos com a mão

Use as poses das articulações da mão para detectar gestos básicos. Consulte as Convenções das articulações das mãos para determinar em qual intervalo de posições as articulações precisam estar para serem registradas como uma determinada pose.

Por exemplo, para detectar um gesto de pinça com o polegar e o indicador, use a distância entre as duas articulações da ponta:

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

Um exemplo de gesto mais complicado é o gesto de "parar". Nesse gesto, cada dedo precisa estar esticado, ou seja, cada articulação de cada dedo precisa estar apontando aproximadamente para a mesma direção:

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)

Considere os seguintes pontos ao desenvolver a detecção personalizada de gestos com a mão:

  • Os usuários podem ter uma interpretação diferente de qualquer gesto. Por exemplo, alguns podem considerar um gesto de "parar" com os dedos abertos, enquanto outros podem achar mais intuitivo manter os dedos juntos.
  • Alguns gestos podem ser desconfortáveis de manter. Use gestos intuitivos que não sobrecarreguem as mãos do usuário.

Determinar a mão secundária do usuário

O sistema Android coloca a navegação do sistema na mão principal do usuário, conforme especificado pelo usuário nas preferências do sistema. Use a mão secundária para seus gestos personalizados para evitar conflitos com os gestos de navegação do sistema:

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)