카메라 회전(시점 이동) 기능 분석(1)
3D 공간에서 자유롭게 돌아다니려면 시점 이동 기능은 필수로 제공되어야 한다. Collab Viewer 템플릿에는 키보드&마우스 뿐 아니라 터치 스크린 입력과 VR 컨트롤러에 대해서 시점 이동 로직이 구현되어있다.
본 포스팅에서는 마우스 우클릭을 이용한 카메라 회전에 대해 분석해본다. 이번 (1)편에서는 비행(Fly) 및 보행(Walk) 모드 까지만 분석하고 다음 (2)편에서 선회(Orbit) 모드를 분석한다.
목차
1. 살펴보기
2. 입력 매핑
3. 기본 폰에서의 이벤트 처리
4. 비행 모드
5. 보행 모드
6. 마무리
1. 살펴보기
위에서도 말했듯이 일반적인 키보드/마우스 입력 환경에서는 마우스 우클릭을 통해 시점 이동을 한다. 정확히는 오른쪽 버튼을 누른 채 마우스를 이리 저리 움직이는 방식이다. 동영상을 통해 각각의 모드(Fly, Walk, Orbit)에 따라 카메라 회전이 어떻게 이루어 지는지 확인해보자.
영상에서 보다시피 Fly, Walk 모드와 Orbit 모드는 서로 다른 방식으로 회전한다. Fly, Walk 모드는 내가 중심이 되어 주변을 둘러보는 방식이고, Orbit 모드는 정해진 타겟의 주위를 회전한다. 당연히 두 방식의 로직은 다르게 구현되어 있을 것이다.
2. 입력 매핑
RotateView 라는 이름으로 마우스 오른쪽 버튼이 매핑되어있는 것을 확인할 수 있다.
또한 실제 카메라 회전에 필요한 마우스의 X, Y축 이동도 매핑되어있다.
3. 기본 폰에서의 이벤트 처리
BP_BasePawn 클래스 내에 선언된 RotateView 이벤트에서 분석을 시작해보자.
InputAction RotateView
간단한다. 마우스 오른쪽 버튼을 누르면(Pressed) Right Mouse Pressed 이벤트를 실행하고, 버튼을 떼면(Released) Right Mouse Released 이벤트를 실행한다. 그런 뒤에 Is Mouse Rotate View 변수를 알맞게 세팅한다.
RightMousePressed
위 사진은 마우스 오른쪽 버튼을 눌렀을 때 이벤트 처리 과정을 보여준다. 연결된 노드들을 순서대로 알아보자.
1) 델리게이트 호출
가장 먼저 실행되는 노드이다. 마우스 오른쪽 버튼이 눌렸다는 것을 알리는 멀티캐스트 델리게이트(이벤트 디스패쳐)를 호출한다. 그러나 이 델리게이트에는 한개의 이벤트도 바인딩 되어있지 않다.
기능 확장에 대비하기 위해 일단 만들어 둔 장치로 보인다.
2) Is Shooting Laser 검사
마우스 왼쪽 버튼을 누를 때 True로 셋팅되는 IsShootingLaser 변수의 값을 검사한다. 마우스 좌클릭이 눌려있으면 더이상 로직을 진행하지 않는다.
3) Set Is Rotating View to True
IsRotatingView의 값을 True로 만든다. 이 변수가 True일 때 카메라 회전이 활성화 된다. 이는 BP_Desktop_FlyMode_Pawn의 회전을 담당하는 이벤트가 IsRotatingView가 True일 때에만 로직을 실행하기 때문이다.
결국 이 노드는 앞의 노드와 함께 다음과 같은 의미를 만들어 내는 것처럼 보인다.
"좌클릭 중에는 회전을 하지 못한다."
이 때, 의미를 만들어 내는 것처럼 보인다고 말 한 이유가 있다. 다음 영상을 보자.
레이저 포인터를 사용하는 중에도 우클릭을 이용해 회전이 가능하다.
그리고 아래 영상에서처럼 실행 중에 디버깅을 해보면 IsRotatingView의 값 또한 True로 세팅이 된다.
그 이유는 뭘까?
이 문제의 힌트는 처음 생성되는 폰 클래스가 데스크탑 용으로 '보이는' BP_Desktop_FlyMode_Pawn이 아니라 터치 입력까지 지원하는 BP_Touch_FlyMode_Pawn이라는 점에서 찾을 수 있다.
이 클래스의 부모가 바로 BP_Desktop_FlyMode_Pawn이다. 그래서 키보드&마우스 입력에 더해 터치 인터페이스 지원까지 확장한 형태라고 볼 수 있다.
이제 답을 찾아보자.
이 폰(Touch_Fly)에는 Tick 이벤트가 구현되어있다. 잘 알다시피 Tick 이벤트는 매 프레임 마다 실행되는 이벤트이다.
Tick 이벤트에서는 다음과 같은 로직이 돌아간다.
Is Mouse Rotate View 가 True일 때 Is Rotating View 또한 자동으로 True가 된다.
다시 BP_BasePawn의 마우스 우클릭 입력 부분으로 돌아가보면
마우스 오른쪽 버튼을 누르면 IsMouseRotateView 변수가 True로 세팅이 된다는 것을 확인할 수 있다. 결과적으로 마우스 좌클릭을 하든 말든(IsShootingLaser 값에 상관없이) 카메라는 회전을 할 수 있다. 우클릭을 하는 순간 IsMouseRotateView는 True가 되고 IsRotatingView 또한 Tick 이벤트에서 True가 되기 때문이다.
여기서 한가지 의문이 생긴다. 어짜피 Tick Event에서 IsRotatingView를 True로 만들어 주는데, BP_BasePawn에서 조건부로 값을 세팅하는 게 의미가 있을까? 그냥 지워도 되지 않을까?
결론적으로 아주 의미가 없지는 않다고 본다. 템플릿이라는 특성상 여러가지 구현방식을 제시하면 좋고, 어떤 노드든 필요와 목적에 따라 수정 또는 삭제하면 그만이기 때문.
RightMousePressed 이벤트의 나머지 노드를 마저 살펴보자.
4) Set Show Mouse Cursor to False
마우스 커서를 감춘다. 이 노드 또한 레이저 포인터를 발사중일 때는 호출되지 않기 때문에 좌클릭>우클릭을 하면 마우스 포인터가 그대로 보이는 상태로 카메라를 회전할 수 있다.
5) Set LockedMousePosition
마우스 우클릭 시 마우스 포인터의 스크린상의 위치를 저장한다. 화면을 돌리면 커서도 같이 이동하는데 (커서가 보이지 않을 때도 마찬가지다.), 그것을 방지하기 위해 위치를 기억시키는 작업이다.
RightMousePressed
누르고 있던 마우스 오른쪽 버튼을 떼면 이 이벤트가 실행된다. 구현이 간단하기 때문에 한꺼번에 보고 넘어가자.
먼저 IsRotatingView의 값을 검사한다. 위에서 확인 했듯이 우클릭을 하는 중에는 이 값이 무조건 True이다. 값이 True 라면 IsRotatingView를 다시 False로 만든 뒤, 마우스 커서를 다시 보이게 설정한다.
계속해서 비행 모드와 보행 모드의 카메라 회전을 분석해보자. 그러기 위해 BP_BasePawn 클래스를 상속받은 BP_Desktop_FlyMode_Pawn 과 BP_Desktop_WalkMode_Pawn 클래스에 구현된 카메라 회전 로직을 살펴본다.
4. 비행 모드
비행 모드의 회전을 분석하기 위해 BP_Desktop_FlyMode_Pawn에 구현된 카메라 회전 로직을 확인해보자.
LookUp
LookUp 이벤트는 마우스의 Y축(위-아래)이동에 따라 Axis Value를 내보낸다.
기억이 안난다면 2. 입력 매핑을 다시 확인해보자. Mouse Y(Scale 1.0)에 LookUp 이벤트가 매핑돼있다.
1) Is Rotating View 검사
앞에서 살펴본 것 처럼 카메라가 회전하기 위해서는 Is Rotating View가 True에 맞춰져 있어야 한다. 그리고 마우스 오른쪽 버튼을 누르고 있을 때는 무조건 True가 되는 것도 위에서 확인했다.
2) Lock Mouse to Pressed Location
상위 클래스인 BP_BasePawn에 구현되어있는 이벤트이다. 선언부로 가보자.
마우스 커서의 위치를 LockedMousePosition(2D Vector)에 저장된 값으로 설정한다. 앞서 RightMousePressed 이벤트를 살펴볼 때 마우스 오른쪽 버튼을 누르면 커서의 위치가 저장된다는 것을 확인했다. 그 위치값을 여기에서 사용한다.
3) Add Controller Pitch Input
Add Controller Pitch Input을 이용해 폰에게 상하 회전을 일으킨다.
좀 더 자세하게 설명하면,
이 노드는 폰에게 붙어있는(빙의된, Possessed) 컨트롤러를 위 아래로 회전(Pitch Rotation)하게 만드는 노드이다. 이 때, 컨트롤러의 상하 회전은 폰에 동기화 되는데 이는 아래 옵션이 켜져있기 때문이다.
그래서 폰이 회전하기 이전에 컨트롤러가 회전하고 폰이 그것을 따라가는 형식의 회전 방법이다.
이번에는 FInterp To 노드를알아보자. FlyMode에서는 카메라의 회전을 부드럽게 만들어 줄 수 있는데 말로 설명하기 어려우니 비교영상으로 알아보자.
부드러운 회전에서는 회전을 시작할때와 끝날 때 카메라의 속도가 느려진다. 이것을 가능하게 하는게 바로 FInterp To이다.
FInterp To는 Current와 Target 두 Float 값을 보간한다. 쉽게 말하면 두 Float 값의 사이를 촘촘히 채워준다. 그렇게 만든 값은 Add Controller Pitch Input 노드에 전달된다.
예를들어 Current = 0 , Target = 1.0 이면 0.1, 0.2, 0.3, 0.4 처럼 0에서 1.0로 변하는 과정에서 중간값들을 만들어서 내보낸다. 그리고 순차적으로 내보낼 때 반드시 필요한 게 Delta Time이다. 시간의 흐름에 따라 값을 내보내기 때문이다. Interp Speed는 Current에서 Target에 얼마나 빠르게 도달할지를 결정한다. 중간값을 촘촘하게 만들지, 듬성듬성 만들지 결정한다.
전체적인 작동방식은 아래에서 다음노드까지 보고 설명한다.
4) Set Previous Ymouse
FInterp To로 보간한 값들을 Add Controller Pitch Input 노드에 들어간 뒤 Previous Ymouse 변수에 저장된다. 저장된 Previous Ymouse의 값은 다음 프레임 LookUp 이벤트 호출시 FInterp To 노드에 Current 값으로 사용된다.
이번엔 Target이 어떻게 만들어 지는지 알아보자.
Axis Value는 마우스 위 아래 이동과 멈춘 상태에 따라 값을 매 프레임 출력한다.
이 때 위 이동은 음수(-) 아래 이동은 양수(+) 멈춤은 0으로 출력된다.
LookSpeed는 기본값이 1.0이다. 카메라 회전 속도를 높이고 싶다면 이 값을 1.0 이상으로 설정해주면 된다.
마지막으로 -1.0을 곱하는 이유는 마우스 위 이동이 (-), 아래 이동이 (+)이기 때문이다. -1.0을 곱하지 않으면 마우스를 올릴 때 카메라는 내려간다.
Delta Time에 사용된 Get World Delta Seconds는 이전프레임과 현재프레임의 시간차를 초단위로 출력한다. 일반적으로 FInterp To 같은 보간 노드를 쓸 때는 Delta Time에 이 노드를 사용한다.
Interp Speed에는 상수 30.0이 사용됐다.
30.0은 부드러운 회전의 느낌을 거의 못느낄 만큼 빠른 속도다. 위에서 본 부드러운 회전 영상은 이 값을 3.0으로 맞춰놓고 찍었다.
다시 한번 노드망을 전체적으로 보자
결론적으로 이 노드망은 카메라의 상하 회전 위치를 결정하는 로직이다. 그런데 회전값을 즉각적으로 설정하는 게 아니라. FInterp To가 출력해주는 값에 따라 순차적으로 회전값을 적용한다. 이전 프레임에 사용된 회전값(Previous Ymouse)은 다음 프레임에서 현재 값(Current)가 되어 새로운 Target을 향해 다시 순차적으로 이동해나간다. 그것을 마우스 오른쪽 버튼이 떼어질 때(Released) 까지 반복한다.
5) Lookat ROS 호출
내가 카메라를 회전했다면 상대방에게도 그 정보를 전달할 필요가 있다. 그래서 마지막 노드는 멀티플레이어 처리를 한다.
Visual Camera Root는 멀티플레이 환경에서 다른 사람들에게 보여지는 카메라의 중심점이다.
나의 회전 값 중 Pitch(상 하 회전) 값을 Get Control Rotation을 통해 구하고 그것을 서버에서 실행되는 Lookat_ROS 이벤트를 호출하면서 전달한다.
그런데 여기서 의문점이 하나 발생한다.
멀티플레이 환경에서 폰(또는 캐릭터) 인스턴스는 Replicates 옵션이 켜져있어야 하고, 이동 및 회전 또한 알아서 복제가 된다. 실제로 Lookat_ROS 노드의 실행핀을 끊어 놓고 테스트해 봐도 레플리케이션이 잘 일어나는 것을 확인할 수 있다.
굳이 또 RPC를 이용해서 회전값을 전달할 필요가 없을 것 같다는 게 나의 의견이다. 또한 동기화가 필요하다면 Pitch 값 뿐 아니라 좌우 회전값 인 Yaw도 동기화를 해주어야 하는데, 그렇게 구현되지 않았다. 결론적으로 이 부분은 조금 더 생각해 볼 필요가 있다.
LookRight
이번에는 LookRight 이벤트를 알아보자. (좌우 회전이기 때문에 Turn 이라고도 한다)
LookUp 함수와 구조가 거의 동일하다. 차이점이라면 FInterp To의 Target을 계산할 때 -1.0을 곱하지 않는다는 점. 그리고 좌우 회전이기 때문에 Add Controller Pitch Input 이 아닌 Yaw Input을 사용했다는 점이 있다.
5. 보행 모드
보행 모드의 회전 로직은 앞서 알아본 비행 모드의 그것과 매우 유사하다.
LookUp
기본적인 구조는 Fly 모드와 동일하다. 차이가 있다면 회전 값을 결정할 때 FInterp To를 사용하지 않았다는 것이다. 이 부분도 역시 '템플릿'이기 때문에 여러가지 방법을 보여줬다는 측면에서 긍정적으로 해석해 볼 수 있다.
마지막 Server Event Look Up만 보고 넘어가자.
서버에서 실행되는 이벤트를 호출하면서 Head Root 컴포넌트와 Pawn의 Control Rotation 값을 전달한다.
Head Root 컴포넌트는 뷰포트에서 확인할 수 있다.
서버에서 호출된 위 이벤트는 Head Root 컴포넌트를 전달 받은 값 만큼 상하회전시킨다. (New Rotation 인자를 쪼개서 Pitch 값만 사용한다.)
즉, 서버에 존재하는 캐릭터의 머리가 상하로 회전하고 그것이 각 클라이언트에게 Replicate 된다. 다른 참가자들은 해당 플레이어가 어디를 보는지 시각적으로 확인할 수 있게 되는 것이다. 영상으로 확인해보자.
이 때, 재밌는 것은 머리가 위 아래로 회전할 때 상체 또한 어느정도 회전을 한다는 점이다. 이 로직은 Tick 이벤트에 구현되어 있다.
LookRight
LookRight의 구조는 이미 앞서 분석한 내용을 바탕으로 쉽게 파악할 수 있다.
6. 마무리
이번 편에서는 비행모드와 보행모드의 카메라 회전을 모두 분석했다. 기본적으로 BasePawn에서 시작하는 회전로직은 비행, 보행모드 각각에서 개별 로직에 의해 회전값이 결정된다.
비행 모드는 FInterp To를 사용해서 부드러운 회전의 구현을 가능하게 했다. 서버에서 실행되는 이벤트는 굳이 호출하지 않아도 카메라 회전이 동기화 되는 것도 확인했다.
보행 모드에서는 고개의 위 아래 움직임이 구현되어 있었고, 멀티플레이 환경에서도 잘 동작하는 것을 확인했다. Head의 회전에따라 Torso의 회전이 자연스럽게 이루어 지도록 세심하게 만든점도 흥미로웠다.
선회(Orbit) 모드 분석은 분량 문제로 다음편에서 진행된다.
'비현실 연구소 > [2층]샘플 분석실' 카테고리의 다른 글
Collab Viewer 템플릿 분석: 레이저 포인터 (0) | 2020.10.28 |
---|---|
Collab Viewer 템플릿 분석: 게임모드 (0) | 2020.10.27 |
Collab Viewer 템플릿 분석: 시리즈 소개 (0) | 2020.10.27 |