킹머핀의 제작 일지
VR) 유니티 XR Interaction Toolkit 응용 탐구 본문
위에서 LocomotionProvider을 상속받는 네 가지 클래스의 핵심 코드는 공통적으로 Update 함수에서 실행된다. 단, TeleportationProvider만 protected virtual void이고, 나머지는 protected void이다. 이게 무엇을 의미하는가?!
우선 virtual은 재정의 함수. 그러니까 TeleportationProvider을 재정의(확장)해서 사용하란 말인데, 이 클래스를 상속받는 다른 텔레포테이션 관련 스크립트가 없다. 그 말은.. 내가 상속받을 기회를 주는 건가..?
protected는 상속 관계에서만 사용할 수 있다는 접근자. 나머지 세 개의 스크립트에서는 재정의 함수가 아니니 접근만 하거나(있는 그대로 쓰거나) new 키워드로 무시하고 다시 정의할 수 있다(상속 클래스에서만, override는 상위 클래스의 함수도 재정의한다).
일단 클래스가 하나밖에 없는 TeleportationProvider을 상속받아서 새로운 기능을 만들어보자. 가장 쉬워보이니까.
내가 만들 기능은 Fast move, 즉시 이동하는 텔레포트와는 달리 시작 지점과 목표 지점 사이를 보간하며 움직인다.
QueueTeleportRequest?
이 클래스에는 함수 하나가 더 있다. (두 개밖에 없어서 감격이다) 그리고 이 함수 역시 virtual이다.
분명 쓸 데가 있으니 있는 함수일 텐데, 지금까지 열어봤던 스크립트 중에서는 이 함수를 호출하지 않는다. 어디에서 호출하고 있는거지? VS for Unity의 '선언으로 이동', '구현으로 이동', '참조 찾기' 등은 에셋 폴더 내 스크립트만 해당하는지 찾을 수 없었다.
그렇게 멍을 때리다시피 Packages - XR Interaction Toolkit 폴더를 뒤지다가 결국엔 찾았다. BaseTeleportationInteractable이라는 클래스다. (이 스크립트를 열어두니 위의 기능이 작동한다.. ㅋㅋㅋ)
이 클래스는 public abstract class이고, XRBaseInteractable를 상속받는다. 위 함수의 파라미터 타입인 TeleportRequest도 여기서 Struct로 선언한다.
그리고 저 'QueueTeleportRequest 함수를 호출하는 함수'를 호출하는 위치가 상위 클래스인데.. 뭐.. 거기까지 갈 필요는 없고.. 중요한 건, QueueTeleportRequest 함수를 호출하는 함수 4개의 이름이 OnSelectEntered, OnSelectExited, OnActivated, OnDeactivated이고, 결과적으로 뭔가 다양한 조건이 부합해서 텔레포트가 가능하면 필요한 정보를 저 함수로 전달하며 Request(요청)한다는 점이다.
막상 이렇게 파헤쳐놓고 보니 나한테 필요한지 모르겠다. 왜 이름이 Queue인지도 모르겠고,, 에휴 일단 내버려 두자..
아 맞다, 거기서 적용하는 변수와 리턴값을 조작해도 되는지 알아보려 했지..? (이래서 글을 쓰면서 목적을 바로바로 정리해둬야 한다)
혹시나 이 함수의 리턴값을 이용하는 스크립트가 있는지 찾을 방법을 모르겠다. 그러니 리턴값은 건드리지 말자. 어차피 인수로 받은 TeleportRequest를 클래스 메소드에 복사하고, Update 함수 내 명령 실행 여부인 vaildRequest(유효한 요청)를 true로 변경하는 코드 말곤 없어서, 내 맘대로 수정해도 될 것 같다.
그래서 난 TeleportationProvider을 상속받아 필요한 부분을 재정의했고, 이 클래스의 이름을 TeleportationOrFastMoveProvider으로 지었다.
오큘러스 퀘스트에 두 가지 경우의 빌드를 설치하고 HMD를 착용해 테스트해보았다.
- 재정의를 했으니 있던 그대로 빌드한 경우 : 변화가 없다.
상위 클래스에서도 재정의한 대로 작동하는 게 아닌가? 엥 그러고보니 추가한 변수는 하위 클래스에 있는데 그게 되나? 아무래도 내가 상속에 대해 잘 모르나보다. 당장 알아봐야겠다.. - 상위 클래스(TeleportRequest)를 비활성화만 하고, 재정의한 클래스를 컴포넌트에 활성화한 후 빌드한 경우 : 변화가 없다.
관련 조작은 가능한데, 이동이 불가능하다. 아직 원인은 모르지만, 적어도 내가 건드린 몇 안 되는 요소에서 문제를 찾으면 될 것이다.이동 불가 문제는 빼먹은 코드 넣어서 해결.
일단 1번에서 변화가 없었던 이유는, 내가 생각한 대로 작동하려면 하위(파생) 클래스를 상위(기본) 클래스로 캐스팅해서 사용해야 한다. 그러니까 그 과정이 없어서 오류도 없고 변화도 없었던 것이다. 그래서 2번 방법을 고쳐서 이용해야 한다.
2번 방법은 왜 변화가 없었을까? XR Device Simulater로 로그를 확인해보았는데, Fast Move 시작과 끝에서 출력되어야 할 로그가 보이지 않는다. 애초에 실행도 안 됐나보다. 일단 출력 코드를 여기저기에 넣어보자.
제대로 상속받아야지
로그로 확인해본 결과, 그냥, 텔레포트가 작동하고 있었다. 내가 짠 코드는 왜 작동 안 한 거야? 확인해보자.. 그러고 백업해둔 프로젝트를 열어 같은 조건에서 실행해봤는데.. 텔레포트도 작동 안 한다. 기존 프로젝트로 다시 시도해보았다. 작동 안 한다.. 뭐야? 또다시 코딩의 악몽이 시작된다..!
침착하고, 증세부터 확인해보자.
- QueueTeleportRequest 함수부터 작동하지 않는다.
- TeleportationProvider를 활성화하면 마지막으로 지정한 위치로 이동한다.
기존 패키지가 재정의한 클래스가 아닌 기존 클래스(TeleportationProvider)만 접근하는 것으로 보인다. 그래서 BaseTeleportationInteractable를 확인해보았는데, 역시 Awake에서 TeleportationProvider만을 가져오고 있었다.
이걸 어떻게 해결하지? BaseTeleportationIneractable도 재정의해야 하나? 그럼 이 클래스에도 똑같은 문제가 생길지도.. 클래스를 재정의하는 방법은 없나? 검색해보니 없는 듯 하다. 그럼 일단 이 클래스부터 상속받는 모든 하위 클래스까지 재정의하는 수밖에.
BaseTeleportationInteractable의 Awake는 protected override고, 상위 클래스인 XRBaseInteractable의 Awake를 base.Awake로 부른다. 중요한 거니까 부르겠지? 그러니까 BaseTeleportationInteractable을 상속받아서 그 Awake도 base.Awake로 부른 다음 위에서 재정의한 클래스를 메소드로 가져온다. 그럼 준비 끝.
BaseTeleportationInteractable을 상속받는 두 클래스인 TeleportationAnchor, TeleportationArea를 복제해서 방금 새로 만든 클래스를 상속받는다. 그리고 이 두 클래스를 사용하는 컴포넌트를 전부 새로 만든 클래스로 바꾼다.
결과는? 뭔가 달라졌다. 일단 텔레포트가 가능한 곳에 조준선을 쏘면 드디어 조준선이 초록색이 된다. 그런데 그 곳과 그 영역에 파란색 기즈모를 그리는데, 지금은 안 그려진다. 아무래도 Anchor과 Area 클래스에서 제대로 작동하는.... 아, SendTeleportRequest 함수! 이 함수를 호출하는 4개의 함수를 TeleportationOrFastMoveProvider에 넣지 않았다.
이 4개의 함수는 한정자가 protected internal override다. internal은 같은 어셈플리 파일(같은 프로젝트) 내에서만 접근이 가능하다. 아니 근데, 조금 더 알아보니 protected와 internal이 같이 쓰이면 포함하는 클래스에서 파생된 형식에서도 접근할 수 있다고 한다. 맘에 안 든다.
근데 Assets 폴더에 있는 내 스크립트와 Packages 폴더(실제로는 Library - PackageCache)에 있는 VR Interaction Toolkit 스크립트는 '같은 어셈플리 파일'이 확실하나? 두 폴더의 상위 폴더가 솔루션이긴 한데, VR Interaction Toolkit 스크립트를 열어도 Packages 폴더는 표시가 안 된다. 아, 그거 상관없구나. 상속 클래스에서도 접근할 수 있다매. 그럼 override해서 써야겠다. ㅎㅎㅎ
그나저나 엑서스가 포함하는 형식? 엑서스가 포함하는 클래스? 뭔 소리야 대체 참 맘에 안 드네. 나만 못 알아먹겠나?
오, 이제 QueueTeleportRequest 함수가 불려진다. 위의 4개의 함수가 제대로 작동했다는 뜻. 하지만 파란색 기즈모는 아직도 안 그려진다. 그래도 작동한다는 게 어딘가!
BeginLocomotion
현재 이 함수가 아무 조작 없이도 10초에 한 번씩 true를 반환하고 있다. 원래 이랬나? 왜 10초지? 일단 기즈모 표시 문제부터 알아보자.
OnDrawGizmos
그러고보니 Anchor과 Area 클래스의 함수는 어디서 호출하지? OnValidate, OnDrawGizmos 함수에 마우스를 대보니 유니티 자체 한글 설명이 표시된다. 그럼 이건 패키지 내 클래스에서 부르는 함수가 아니다.
OnDrawGizmos 함수의 설명은 다음과 같다.
유니티 이벤트 함수 실행 순서 메뉴얼을 확인해보니 예상대로 매 프레임마다 자동으로 호출된다.
그걸 알기 전에는, Ray 관련 클래스에서 이 함수를 호출하는 줄 알고 이전에 썼던 글에서 이에 대한 정보를 찾아보았는데..
세상에, 인스펙터에서 Interactable Events로 기즈모를 껐다 켰다 하는 거였잖아?! 저 글 쓰길 잘 했다.. 또 보길 잘했다..
그 말은, 내가 새로 정의한 스크립트(TeleportationOrFastMoveAnchor, Area)도 똑같이 추가해주면 해결된다는 뜻!
오, 그렇게 하니 조준한 지점에 파란색 위치 기즈모는 여전히 안 뜨지만, 해당 영역의 파란색 테두리는 표시가 된다.
다른 옵션도 살펴보니, Custom Reticle에 teleportReticle 프리팹이 연결되어 있었다. 이 역시 똑같이 연결해주면 파란색 위치까지 잘 나타난다!
..엥? 갑자기 Fast move가 작동한다?? 그것도 위에서 봤던 대로 10초마다 작동한다?!
BeginLocomotion
일단 왜 10초인지부터 알아보자.
초기에 Locomotion System의 timeout 실수가 10으로 정해져 있다. 이 변수에 대한 설명은 아래와 같다.
The timeout (in seconds) for exclusive access to the XR Rig. (XR Rig에 독점적으로 접근하기 위한 초 단위의 대기시간.)
이게 있으면 왜 기존에는 빠르게 이동과 회전을 반복할 수 있었을까? 이 스크립트에는 RequestExclusiveOperation(The locomotion provider that is requesting access)와 FinishExclusiveOperation(The locomotion provider that is relinquishing access)라는 함수가 있는데, 각각 상위 클래스인 LocomotionProvider의 BeginLocomotion과 EndLocomotion 함수에서 호출한다. (또 찾는 데 한참 걸렸는데, 진작에 이 함수부터 시작해서 찬찬히 찾을걸 그랬다)
TeleportationOrFastMoveProvier에서 BeginLocomotion이 잘 작동하는지 조건문으로 호출하는 코드를 주석 처리해봤다. 그랬더니 이 문제가 사라졌다?! 원래 Update 함수 내 명령을 수행할지 판단하기 위해서 한 번씩 불리던 함수인데, 내가 한 번씩 더 호출해서 뭔가 문제가 생겼나보다.. 원인이 뭘까? 일단 호출한 함수 내 명령이 연속으로 두 번 이상 실행되면 곤란하기 때문일 것이다. 구체적으론 알아보지 말자..
왜 조금 후퇴한 뒤에 전진하는 거니
드디어 잘 작동한다! 와! 근데 Fast Move가 시작하는 순간 제자리에서 약간 뒤로 이동한 뒤에 앞으로 이동한다. 거슬리니까 해결해보자.
현재 시작점은 xrRig.rig.transform.position + heightAdjustment.
XR Device Simulater로 카메라(HMD)를 멀리 이동시킨 다음 Fast move를 해보면 약간 앞으로 갈 때도 있고, 처음 시작했을 때 똑같이 멀리 이동시킨 다음 시도해보니 대략 원위치에서 시작하는 것으로 보인다. 아.. 이게 뭐지.. 으어.. (집중력 증발)(아 이럴 때는 다른 일을 해야하는데)
현재 상황을 게임오브젝트 계층 별로 정리해보자.
- XRRig/CameraOffset/Main Camera : HMD 역할.
- XRRig/CameraOffset : XRRig 스크립트의 Y offset대로 조절하는 역할로 추정. Z offset의 초기값은 -1.438.
- XRRig : '전역 좌표계에서 Main Camera가 CameraOffset과 동일한 위치'인 좌표로 이동한 후 fast move 한다. (실제 이동하는 오브젝트)
우선 XRRig가 Main Camera의 위치를 기준으로 이동해서도 안 되고, CameraOffset의 위치를 기준으로 움직이기 시작해서도 안 된다. 휴, 다른 일을 하다 오니 머리가 잘 돌아가는 느낌이네.
전자는 좌표계의 불일치, 후자는 시작점의 위치. 각각 고쳐보자.
텔레포트의 결과는 어떤가. 만약 카메라가 리그에서 한참 멀어진 채로 텔레포트를 하면, 카메라가 목표 지점으로 이동하고 리그는 카메라와의 거리를 여전히 유지할 것이다. 하지만 실제로는 리그가 이동했다. 즉 카메라와 목표 지점의 전역 좌표가 동일하다. 그렇다면 시작 지점 또한 카메라가 기준이어야 할 터.
XRRig gameobject's position is (0.0, 1.0, 0.0)
XRRig/CameraOffset gameobject's position is (0.0, 1.36144, -1.438)
XRRig/CameraOffset/Main Camera gameobject's position is (0.0, 0.0, 0.0)
xrRig.cameraInRigSpacePos is (0.0, 1.4, -1.4) //The camera's local position in rig space
xrRig.rigInCameraSpacePos is (-, -, -) //고개를 돌린 정도에 따라 다름
xrRig.rig.transform.position is (0.0, 1.0, 0.0) //XRRig gameobject's position
if xrRig.MoveCameraToWorldLocation(xrRig.cameraInRigSpacePos),
XRRig gameobject's position is (0.0, 0.0, 0.0) //아래로 1 내려감
if xrRig.MoveCameraToWorldLocation(xrRig.rig.transform.position),
XRRig gameobject's position is (0.0, -0.36144, 1.438)
이건 각 게임오브젝트의 초기 위치와 그로부터 가져온 각 위치값의 결과다.
xrRig.cameraInRigSpacePos가 CameraOffset 오브젝트의 좌표와 정확히 일치하지는 않지만 소수점 반올림하면 같다. 반올림돼서 보이는 이유는 print 함수 때문인 듯 하다.
xrRig.cameraInRigSpacePos로 MoveCameraToWorldLocation 했더니 XRRig 오브젝트가 원점으로 갔다.
xrRig.rig.transform.position으로 MoveCameraToWorldLocation 했더니 저기 위와 같은 위치로 이동했다.
XRRig 오브젝트가, 인수로 넣은 좌표에서 CameraOffset 좌표를 뺀 위치로 이동한다. 그럼 이 계산을 반대로 하면?
Vector3 startPoint = xrRig.rig.transform.position + xrRig.cameraInRigSpacePos;
헐 미친 제대로 작동한다. 전혀 될 줄 몰랐다. 어제 하루종일 해도 해결 못 했는데?
기대도 안 했는데???
마침 MoveCameraToWorldLocation의 설명을 이제와서 발견해가지고 이제라도 읽으려고 파파고 번역도 해놨는데??
/// This function moves the camera to the world location provided by desiredWorldLocation.
/// It does this by moving the rig object so that the camera's world location matches the desiredWorldLocation
/// the position in world space that the camera should be moved to
/// Returns true if the move is performed. Otherwise, returns false.
/// 이 기능은 카메라를 원하는 WorldLocation에서 제공하는 세계 위치로 이동합니다.
/// 카메라의 세계 위치가 원하는 세계 위치와 일치하도록 리그 객체를 이동하여 이 작업을 수행합니다.
/// 카메라를 이동해야 하는 세계 공간에서의 위치
/// 이동이 수행된 경우 true를 반환합니다. 그렇지 않으면 false를 반환합니다.
(다음부터는 코드를 해석하기 어려우면 꼭 제공하는 설명이 있는지부터 찾아보자.)
뭐 이건 지금 읽어보기로 하고.
결국 완성했다. 마지막으로 할 말은..
프로그래밍 망해라.
참고 : XRRig 클래스의 cameraInRigSpacePos는 Rig의 위치를 카메라의 위치로부터 상대적으로(지역 좌표로, InverseTransformPoint) 변환한 결과다. 물론 rigInCamerSpacePos는 반대. 그래서 고개를 돌린 정도에 따라 위치가 달라졌나보다.
다음 글에서 계속.
(5월 19일 추가 : 문제 발견. Snap turn으로 회전하면 Fast Move의 시작 위치가 바뀐다. 다음 글에서 Smooth Move도 구현했는데, 오큘러스에 빌드해서 테스트해보니 HMD 회전이 반영되지만 무언가에 의해 처음 각도로 돌아와서 강제로 한 방향밖에 바라볼 수 없다. 어디에서 문제가 발생했는지 알아봐야 한다..
다다음 글에서 계속............)
'Unity' 카테고리의 다른 글
UNT) 커스텀 에디터를 만들 때 Undo가 안 된다? (0) | 2021.05.14 |
---|---|
VR) 유니티 XR Interaction Toolkit 계속 탐구 (0) | 2021.05.11 |
VR) 유니티 XR Interaction Toolkit 클래스 탐구 (0) | 2021.04.19 |
UNT) 커스텀 에디터 만들기 (0) | 2021.04.14 |
VR) 유니티 XR Interaction Toolkit 기본 탐구 (0) | 2021.04.05 |