[OpenXR] Custom Hand Pose (Mirror Pose) with XR Interaction Toolkit 2.4.3
VR/XR Interaction Toolkit 2024. 1. 1. 01:08https://youtube.com/playlist?list=PLTFRwWXfOIYBIPKhWi-ZO_ITXqtNuqj6j&si=XhyHS26tNDP6u7CA
이것은 오큘러스 Integration SDK와 비슷하게 만드는것이다
새 씬을 만들고 메인 카메라를 제거후 XR Origin을 생성 한다
Left/Right Controller를 선택후 XR Controller를 제외한 나머지 컴포넌트를 제거 한다
다음과 같은 구조로 만들고 핸드 프리팹을 넣어 준다
Left Controller / Hand Visual / Offset을 선택후 Rotation 90, 0, -90으로 설정 한다
Right Controller / Hand Visual / Offset을 선택후 Rotation -90, 0, -90으로 설정 한다
실행후 결과를 확인 한다
Left/Right Controller를 선택후 XR Controller의 Model Prefab을 None으로 설정 한다
실행후 결과를 확인 한다
프로젝트 창에서 Direct Interactor를 검색해 프리팹을 Left, Right Controller자식으로 넣어준다
Direct Controller를 선택해 Sphere Collider의 Radius값을 0.1로 설정 한다
빈 오브젝트 (Grabbable Cube)를 만들고
자식으로 빈오브젝트 Visuals를 만들고 그 자식으로 Cube를 생성한다
큐브의 크기를 0.1로 만들어 주고
Grabbable Cube를 선택해 위치를 조절 한다
Grabbable Cube를 선택하고
Rigidbody, XR Grab Interactable을 부착 한다
Use Gravity는 체크 해제 하고
Throw on Detach도 체크 해제 하자
실행후 결과를 확인하고
프로젝트 창에서 Interaction Affordance를 검색해 Grabbable Cube자식으로 넣어 주고
Interaction Affordance 자식으로 있는 Color Affordance 를 선택해 Renderer프로퍼티에 Cube를 넣어 준다
Color Affordance Theme를 살펴 보면 각 상태에 따라 컬러를 변경해준다
실행해 결과를 확인 하자
Grabbable Cube를 선택 하고 빈 오브젝트 (Hand Pose)를 만들어 주고
Left/Right Controller에서 사용했던 프리팹과 동일한 프리팹을 넣어 준다
이제 핸드를 선택하고 위치를 적당히 잡아 준다
HandPose 스크립트와 Editor폴더를 만들고 EditorHandPose스크립트를 만든다
다음과 같이 작성하고
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HandPose : MonoBehaviour
{
[SerializeField] private GameObject attachPointPrefab;
[SerializeField] private Transform wrist;
public void CreateAttachPoint()
{
var point = Instantiate(attachPointPrefab, this.transform);
point.transform.localPosition =this.transform.InverseTransformPoint(wrist.position);
}
}
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(HandPose))]
public class EditorHandPose : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
HandPose script = (HandPose)target;
if (GUILayout.Button("Create Attach Point"))
{
script.CreateAttachPoint();
}
}
}
Hand Pose L 을 선택해 Hand Pose컴포넌트를 부착 한다
계층뷰에서 Sphere를 하나 생성하고 크기를 0.01로 설정한후 콜라이더를 제거 하고 이름을 AttachPoint로 변경후 프리팹으로 만들어 준다
Hand Pose L 을 선택후 프로퍼티를 할당한다
이제 Create Attach Point버튼을 누르면 b_l_wrist위치에 Attach Point오브젝트가 생성된다
HandData 스크립트를 생성후
다음과 같이 작성한다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HandData : MonoBehaviour
{
public enum HandType
{
Left, Right
}
public HandType handType;
public Transform[] bones;
public Transform root;
public Animator anim;
}
핸드 포즈 아래 핸드를 선택후 부착 한다
Animator를 제거 하고 Root를 할당 한다
인스펙터를 잠그고
17개의 본을 선택후
HandData의 Bones프로퍼티에 드레그 드롭 한다
이제 핸드 잠금을 해제 한다
이번에는 Left Controller아래 있는 핸드를 선택하고 HandData를 부착 하고 프로퍼티를 넣어 주자
GrabInteractable 스크립트를 만들고
다음과 같이 작성한후
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class GrabInteractable : XRGrabInteractable
{
[SerializeField]
private Transform LeftHandAttachTransform;
[SerializeField]
XRDirectInteractor LeftController;
[SerializeField]
XRDirectInteractor RightController;
protected override void OnSelectEntering(SelectEnterEventArgs args)
{
if (args.interactorObject == LeftController)
{
Debug.Log($"Left hand");
attachTransform.SetPositionAndRotation(LeftHandAttachTransform.position, LeftHandAttachTransform.rotation);
}
else if (args.interactorObject == RightController)
{
Debug.Log($"Right hand");
}
base.OnSelectEntering(args);
}
}
Grabbable Cube를 선택하고 XR Grab Interactable을 제거 후 부착 한다
Grabbable Cube를 선택하고 빈오브젝트 Attach Point를 생성한다
다음과 같이 프로퍼티를 할당한다
DirectInteractor 스크립트를 생성후
다음과 같이 작성하고 Direct Interactor를 선택하 XR Direct Interactor를 제거 하고 부착 한다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class DirectInteractor : XRDirectInteractor
{
public HandData.HandType handType;
}
그리고 Hand Type을 변경 한다
HandPose스크립트를 다음과 같이 수정 한다
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
public class HandPose : MonoBehaviour
{
[SerializeField] private GameObject attachPointPrefab;
[SerializeField] private Transform wrist;
[SerializeField] private GrabInteractable grabInteractable;
[SerializeField] private HandData handDataPose;
private void Start()
{
grabInteractable.selectEntered.AddListener((arg) =>
{
var directInteractor = arg.interactorObject as DirectInteractor;
if (directInteractor.handType == this.handDataPose.handType)
{
Debug.LogFormat("Select Entered : {0}", arg.interactorObject);
var handData = arg.interactorObject.transform.parent.GetComponentInChildren<HandData>();
Debug.LogFormat("handData : {0}", handData);
handData.anim.enabled = false;
handData.root.parent.localRotation = handDataPose.root.localRotation;
for (int i = 0; i < handDataPose.bones.Length; i++)
{
handData.bones[i].localRotation = handDataPose.bones[i].localRotation;
}
}
});
grabInteractable.selectExited.AddListener((arg) =>
{
var directInteractor = arg.interactorObject as DirectInteractor;
if (directInteractor.handType == this.handDataPose.handType)
{
Debug.LogFormat("Select Exited : {0}", arg.interactorObject);
var handData = arg.interactorObject.transform.parent.GetComponentInChildren<HandData>();
Debug.LogFormat("handData : {0}", handData);
handData.anim.enabled = true;
//초기화
handData.root.localPosition = Vector3.zero;
if (this.handDataPose.handType == HandData.HandType.Left)
handData.root.parent.localRotation = Quaternion.Euler(new Vector3(90, 0, -90));
}
});
}
public void CreateAttachPoint()
{
var point = Instantiate(attachPointPrefab, this.transform);
point.transform.localPosition = this.transform.InverseTransformPoint(wrist.position);
}
}
핸드 포즈를 눌러 프로퍼티를 할당한다
Grabbable Cube를 선택해 Grab Interactable 프로퍼티를 넣어준다
핸드 포즈 자식의 핸드를 선택해 Hand컴포넌트를 제거 한다
마지막으로 핸드 포즈 자식의 핸드의 메터리얼을 변경하고 이름을 GhostHandL로 변경 한다
실행후 결과를 확인한다
이제 손을 조금 구부려 본다
위치가 변경되었다면 반드시 핸드 포즈의 Attach Point를 제거 하고 다시 만들어 주자
그리고 Grabbable Cube를 선택해 Grab Interactable 의 Left hand Attach Transform에 할당해줘야 한다
실행후 결과를 확인 한다
오른손도 테스트
오른손 미러 포즈 만들기
MirrorHandPose, EditorMirrorHandPose스크립트를 생성하고
작성한다
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class MirrorHandPose : MonoBehaviour
{
[SerializeField] private HandData handDataPoseFrom;
[SerializeField] private HandData handDataPose;
public void UpdateMirrorPose()
{
Vector3 mirroredPosition = handDataPoseFrom.root.localPosition;
mirroredPosition.x *= -1;
Quaternion mirroredQuaternion = handDataPoseFrom.root.localRotation;
mirroredQuaternion.y *= -1;
mirroredQuaternion.z *= -1;
// Z축을 중심으로 180도 회전하는 쿼터니언 생성
Quaternion rotate180 = Quaternion.Euler(0, 0, 180);
// 반전된 회전에 추가 회전 적용
mirroredQuaternion = rotate180 * mirroredQuaternion;
handDataPose.root.localPosition = mirroredPosition;
handDataPose.root.localRotation = mirroredQuaternion;
for (int i = 0; i < this.handDataPoseFrom.bones.Length; i++)
{
this.handDataPose.bones[i].localRotation = handDataPoseFrom.bones[i].localRotation;
}
}
}
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(MirrorHandPose))]
public class EditorMirrorHandPose : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
MirrorHandPose script = (MirrorHandPose)target;
if (GUILayout.Button("Update Mirror Pose"))
{
script.UpdateMirrorPose();
}
}
}
왼손 포즈를 복사 하고
HandData를 넣어 주고
핸드 포즈 L 에 Mirror Hand Pose를 부착한다
From에 L을 To 에 R을 넣어주고
Update Mirror Pose를 누른다
그리고 각을 잘 잡아 주자
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(MirrorHandPose))]
public class EditorMirrorHandPose : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
MirrorHandPose script = (MirrorHandPose)target;
if (GUILayout.Button("Save Initial Pose"))
{
script.SaveInitialPose();
}
// "Update Mirror Pose" 버튼
if (GUILayout.Button("Update Mirror Pose"))
{
script.UpdateMirrorPose();
}
// "Reset Pose" 버튼 추가
if (GUILayout.Button("Reset Pose"))
{
script.ResetPose();
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class MirrorHandPose : MonoBehaviour
{
[SerializeField] private HandData handDataPoseFrom;
[SerializeField] private HandData handDataPose;
// 초기 상태를 저장하기 위한 변수들
[HideInInspector]
public Vector3 initialRootPosition;
[HideInInspector]
public Quaternion initialRootRotation;
[HideInInspector]
public Quaternion[] initialBoneRotations;
public void ResetPose()
{
// root의 초기 위치와 회전으로 재설정
handDataPose.root.localPosition = initialRootPosition;
handDataPose.root.localRotation = initialRootRotation;
// 모든 bones의 초기 회전으로 재설정
for (int i = 0; i < handDataPose.bones.Length; i++)
{
handDataPose.bones[i].localRotation = initialBoneRotations[i];
}
}
public void UpdateMirrorPose()
{
Vector3 mirroredPosition = handDataPoseFrom.root.localPosition;
mirroredPosition.x *= -1;
Quaternion mirroredQuaternion = handDataPoseFrom.root.localRotation;
mirroredQuaternion.y *= -1;
mirroredQuaternion.z *= -1;
// Z축을 중심으로 180도 회전하는 쿼터니언 생성
Quaternion rotate180 = Quaternion.Euler(0, 0, 180);
// 반전된 회전에 추가 회전 적용
mirroredQuaternion = rotate180 * mirroredQuaternion;
handDataPose.root.localPosition = mirroredPosition;
handDataPose.root.localRotation = mirroredQuaternion;
for (int i = 0; i < this.handDataPoseFrom.bones.Length; i++)
{
this.handDataPose.bones[i].localRotation = handDataPoseFrom.bones[i].localRotation;
}
}
public void SaveInitialPose()
{
// handDataPose의 root와 bones에서 초기 상태 저장
initialRootPosition = handDataPose.root.localPosition;
initialRootRotation = handDataPose.root.localRotation;
initialBoneRotations = new Quaternion[handDataPose.bones.Length];
for (int i = 0; i < handDataPose.bones.Length; i++)
{
initialBoneRotations[i] = handDataPose.bones[i].localRotation;
}
}
}
x 위치에 -1을 곱하는 이유는 3D 공간에서 객체의 거울 이미지를 생성하기 위해서입니다. 유니티에서 3D 객체의 위치는 `(x, y, z)` 형식의 `Vector3`로 표현됩니다. 여기서 x 축은 왼쪽과 오른쪽을, y 축은 위와 아래를, z 축은 앞과 뒤를 나타냅니다.
거울 이미지를 만들기 위해서는 한 축을 기준으로 객체를 '뒤집어야' 합니다. 이 코드에서는 x 축을 기준으로 뒤집습니다. 즉, 객체의 x 위치를 그대로 반대 방향으로 이동시키는 것입니다. 예를 들어, 객체가 원래 x 축에서 +3의 위치에 있었다면, -1을 곱하면 -3의 위치로 이동하게 되어, 거울상에서의 위치가 됩니다.
이러한 변환은 주로 대칭적인 장면이나 객체를 만들 때 사용됩니다. 예를 들어, 캐릭터의 한 손 동작을 다른 손에 정확히 반대로 적용하려 할 때 유용합니다.
회전에서 y축과 z축에 -1을 곱하는 것은 3D 공간에서의 객체의 거울 이미지 회전을 생성하기 위한 것입니다. 유니티에서 객체의 회전은 `Quaternion`으로 표현되며, 이는 복잡한 3D 회전을 나타내는 데 사용되는 수학적 구조입니다.
손 또는 다른 객체를 거울상으로 반전시키기 위해서는 단순히 위치를 반전시키는 것 이상의 조치가 필요합니다. 회전도 반전시켜야 합니다. 3D 공간에서의 회전은 x, y, z 축 주위로 정의됩니다. 거울상 회전을 만들기 위해선, 특정 축(이 경우 y와 z) 주위의 회전 방향을 반대로 해야 합니다.
- **y축 회전 반전:** 객체가 수직(y축)을 중심으로 시계 방향으로 회전했다면, 이를 반대로 반전시키려면 시계 반대 방향으로 같은 각도만큼 회전해야 합니다. 이를 수학적으로 나타내기 위해 y축 회전 성분에 -1을 곱합니다.
- **z축 회전 반전:** 마찬가지로, 객체가 수평(z축)을 중심으로 앞뒤로 회전했다면, 거울상에서는 그 회전이 반대 방향으로 보여야 합니다. z축 회전 성분에 -1을 곱함으로써 이를 달성할 수 있습니다.
이러한 변환을 통해, 객체의 3D 회전이 거울상에서 보는 것처럼 정확하게 반전됩니다. 예를 들어, VR/AR 게임에서 사용자의 한 손 동작을 다른 손에 정확히 반대로 적용하려 할 때 이 방법이 유용합니다.
코드에서 180도 회전을 적용하는 이유는 3D 공간에서의 객체의 거울 이미지를 보다 정확하게 생성하기 위해서입니다. 특히, 이 경우에는 손의 위치와 방향을 거울 이미지처럼 만들기 위해 사용됩니다.
위에서 언급한 것처럼, y축과 z축의 회전 성분에 -1을 곱하는 것만으로는 완벽한 거울 이미지를 생성할 수 없습니다. 이는 객체가 거울을 통해 본 것처럼 완전히 반대 방향을 보도록 하지 않기 때문입니다. 여기서 필요한 것이 바로 Z축을 중심으로 180도 회전하는 추가적인 변환입니다.
Z축 주위로 180도 회전(`Quaternion.Euler(0, 0, 180)`)을 적용하면, 객체가 수평 평면을 기준으로 정확히 반대 방향을 바라보게 됩니다. 이는 객체가 거울에 반사된 것처럼 보이게 하기 위해 필요한 마지막 조정입니다.
결론적으로, y축과 z축 회전 성분에 -1을 곱하고, Z축 주위로 180도 추가 회전을 적용함으로써, 객체는 거울상에서 보는 것처럼 완전히 반대 방향을 바라보게 되며, 이는 거울 이미지 회전을 정확하게 재현하는 데 중요합니다.
public class MirrorPoseUtil
{
// 3D 객체를 거울 이미지로 변환하는 메서드
public static void UpdateMirrorPose(Transform objectTransform)
{
// 위치를 거울 이미지로 반전
objectTransform.localPosition = MirrorPosition(objectTransform.localPosition);
// 회전을 거울 이미지로 반전
objectTransform.localRotation = MirrorRotation(objectTransform.localRotation);
}
// 위치를 거울 이미지로 반전하는 메서드
private static Vector3 MirrorPosition(Vector3 originalPosition)
{
originalPosition.x *= -1;
return originalPosition;
}
// 회전을 거울 이미지로 반전하는 메서드
private static Quaternion MirrorRotation(Quaternion originalRotation)
{
originalRotation.y *= -1;
originalRotation.z *= -1;
// Z축을 중심으로 180도 회전 적용
Quaternion rotate180 = Quaternion.Euler(0, 0, 180);
return rotate180 * originalRotation;
}
}
HandGrabInteractor
HandGrabInteractable
HandGrabPose
Synthetic Hand
테스트 코드들
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(MirrorHandPose))]
public class EditorMirrorHandPose : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
MirrorHandPose script = (MirrorHandPose)target;
if (GUILayout.Button("Update Mirror Pose"))
{
script.UpdateMirrorPose();
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class XRHandedGrabInteractable : XRGrabInteractable
{
[SerializeField]
private Transform LeftHandAttachTransform;
[SerializeField]
private Transform RightHandAttachTransform;
[SerializeField]
XRDirectInteractor LeftController;
[SerializeField]
XRDirectInteractor RightController;
protected override void OnSelectEntering(SelectEnterEventArgs args)
{
if (args.interactorObject == LeftController)
{
Debug.Log($"Left hand");
attachTransform.SetPositionAndRotation(LeftHandAttachTransform.position, LeftHandAttachTransform.rotation);
}
else if (args.interactorObject == RightController)
{
Debug.Log($"Right hand");
attachTransform.SetPositionAndRotation(RightHandAttachTransform.position, RightHandAttachTransform.rotation);
}
base.OnSelectEntering(args);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class MirrorHandPose : MonoBehaviour
{
[SerializeField] private HandData handDataPoseFrom;
[SerializeField] private HandData handDataPose;
public void UpdateMirrorPose()
{
Vector3 mirrorredPosition = handDataPoseFrom.root.localPosition;
Quaternion mirroredQuaternion = handDataPoseFrom.root.localRotation;
handDataPose.root.localPosition = mirrorredPosition;
handDataPose.root.localRotation = mirroredQuaternion * Quaternion.Euler(new Vector3(0, 0, 180));
for (int i = 0; i < this.handDataPoseFrom.bones.Length; i++)
{
this.handDataPose.bones[i].localRotation = handDataPoseFrom.bones[i].localRotation;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
public class Hand : MonoBehaviour
{
[SerializeField] private InputDeviceCharacteristics inputDeviceCharacteristics;
[SerializeField] private Animator anim;
private InputDevice targetDevice;
void Start()
{
this.StartCoroutine(this.WaitForGetDevices());
}
private IEnumerator WaitForGetDevices()
{
WaitForEndOfFrame wait = new WaitForEndOfFrame();
List<InputDevice> devices = new List<InputDevice>();
while (devices.Count == 0)
{
yield return wait;
InputDevices.GetDevicesWithCharacteristics(inputDeviceCharacteristics, devices);
}
this.targetDevice = devices[0];
}
void Update()
{
if (this.targetDevice.isValid)
{
UpdateHand();
}
}
private void UpdateHand()
{
if (this.targetDevice.TryGetFeatureValue(CommonUsages.grip, out float gripValue))
{
this.anim.SetFloat("Grip", gripValue);
}
if (this.targetDevice.TryGetFeatureValue(CommonUsages.trigger, out float triggerValue))
{
this.anim.SetFloat("Trigger", triggerValue);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HandData : MonoBehaviour
{
public enum HandType
{
Left, Right
}
public HandType handType;
public Transform[] bones;
public Transform root;
public Animator anim;
}
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class HandPose : MonoBehaviour
{
[SerializeField]
private XRGrabInteractable grabInteractable;
[SerializeField] private HandData handDataPose;
void Start()
{
grabInteractable.selectEntered.AddListener((arg) =>
{
var directInteractor = arg.interactorObject as DirectInteractor;
if (directInteractor.handType == this.handDataPose.handType)
{
Debug.LogFormat("Select Entered : {0}", arg.interactorObject);
var handData = arg.interactorObject.transform.parent.GetComponentInChildren<HandData>();
Debug.LogFormat("handData : {0}", handData);
handData.anim.enabled = false;
//handData.root.localRotation = handDataPose.root.localRotation * Quaternion.Euler(new Vector3(-90, 0, 90));
handData.root.parent.localRotation = handDataPose.root.localRotation;
for (int i = 0; i < handDataPose.bones.Length; i++)
{
handData.bones[i].localRotation = handDataPose.bones[i].localRotation;
}
//handDataPose.gameObject.SetActive(false);
}
});
grabInteractable.selectExited.AddListener((arg) =>
{
var directInteractor = arg.interactorObject as DirectInteractor;
if (directInteractor.handType == this.handDataPose.handType)
{
Debug.LogFormat("Select Exited : {0}", arg.interactorObject);
var handData = arg.interactorObject.transform.parent.GetComponentInChildren<HandData>();
Debug.LogFormat("handData : {0}", handData);
handData.anim.enabled = true;
//초기화
handData.root.localPosition = Vector3.zero;
if(this.handDataPose.handType == HandData.HandType.Left)
handData.root.parent.localRotation = Quaternion.Euler(new Vector3(90, 0, -90));
if(this.handDataPose.handType == HandData.HandType.Right)
handData.root.parent.localRotation = Quaternion.Euler(new Vector3(-90, 0, -90));
//handDataPose.gameObject.SetActive(true);
}
});
}
}
https://youtu.be/JdspLj4fZlI?list=RDCMUC-BligqNSwG0krJDfaPhytw
https://youtu.be/TW3eAJqWCDU
'VR > XR Interaction Toolkit' 카테고리의 다른 글
[OpenXR] Grab transformers / XR Interaction Toolkit 2.4.3 (0) | 2024.01.02 |
---|---|
[OpenXR] When using Two Hand Grab in XR Interaction Toolkit 2.4.3 (0) | 2024.01.02 |
XR Interaction Toolkit 2.4.3 (Hand Pose) (0) | 2023.12.30 |
XR Interaction Toolkit 2.4.3 (Grab Interactable) (0) | 2023.12.30 |
XR Interaction Toolkit 2.4.3 (Custom Hand Pose) OpenXR (0) | 2023.12.29 |