Search

AR 그랩 시스템

date
2019/11/13
tags
아들과 2인개발
URP
유니티3D
4 more properties
아들과 2인 개발 모음
Search
오랜만의 개발기록 입니다. 그랩 시스템을 넣으면서 게임의 기반도 준비해야 해서 시간이 오래 걸리게 되었네요. 또, AD 님이 자기가 그리고 싶은 그림만 그리는 성향이라 일부 원화가 확보되지 않아서 시간이 걸린 부분도 있습니다. 
그래도 AD 님은 계속 원화를 그리고 있고, 게임 아이디어도 지속해서 내고 있습니다. 아무래도 7살이다 보니 엉덩이 관련 아이템을 넣고 싶은 욕심이 많기는 한데, 계속 설득하면서 갈등을 풀어나가고 있습니다.
AD님이 최근에 그린 원화 몇 장 보고 시작하시죠. 몬스터를 꼬실 때 쓸 과일과 배경을 꾸밀 때 필요한 프랍, 해양생물입니다.
그동안 작업한 내용을 주제별로 3가지 포스팅으로 나누려고 생각 중입니다. 이 포스팅은 그랩 시스템의 구현과 렌더링에 관해 설명하려고 합니다. 우선 그랩 시스템 동영상을 보시죠.

그랩 시스템 구현

그랩 시스템의 기능을 정리하면 이렇습니다.
눈앞에 잡을 수 있는 물체가 있는지 판단하기
잡은 후에 이리저리 이동시키기
이동 시 장애물 피하기
물체를 붙일 수 있는지 판단하기
물체를 붙였다 떼기
놓아주기
하나씩 살펴보면 전체 시스템의 구현 방식을 이해할 수 있을 것 같습니다.

눈앞에 잡을 수 있는 물체가 있는지 판단하기

잡을 수 있는 물체가 있는지 판단하기 위해서는 물리엔진의 Raycast를 사용했습니다. 특히 입체감있는 잡기를 위해서 SphereCast를 사용했습니다. 여기에 추가로, 물체에 Grabbable 컴포넌트가 붙어있는지도 여부도 검사했습니다. Grabbable은 잡을 수 있는 물체인지를 판단하는 용도로도 쓰이지만, 물체의 센터 포인트를 미리 설정하거나, 잡혔을 때의 이펙트와 관련된 정보를 넣어두는 용도로도 사용합니다.

잡은 후에 이리저리 이동시키기

처음에는 간단하게 카메라의 자식으로 붙여서 구현하려고 했습니다. 카메라가 이동하거나 회전할 때 알아서 따라다니니까요. 그런데 잡아끄는 손맛을 내기 위해서 이동을 지연시키니 오히려 방해되어서, 트랜스폼을 직접 계산하는 식으로 변경했습니다.
이동의 기본 개념
기본 개념은 카메라가 이동하거나 회전하더라도 처음 잡았을 때의 거리와 회전을 유지하는 것입니다. 거리를 유지하기 위해서는 최초의 거리를 저장해두었다가, 매 프레임 카메라 트랜스폼의 forward 벡터 방향으로 거리만큼 떨어진 곳에 위치시킵니다. 회전도 마찬가지로 최초 카메라의 로테이션을 저장해두었다가, 그 이후에 카메라가 회전한 만큼 물체도 회전시키면 됩니다. 이렇게 해야 물체가 처음 카메라를 바라보던 그 면이 계속해서 카메라를 바라보게 됩니다.
Lerp를 사용한 이동 딜레이
그런데 이렇게만 하면 잡아끄는 손맛이 부족해서 이동을 조금 지연시켰습니다. 지연은 간단하게 물체의 현재 위치와 가야 할 곳 사이를 Lerp를 통해 보간해주었습니다. 사실 Lerp를 Update 이벤트에서 하면 FPS에 따라서 지연의 정도가 달라지는 문제점이 있습니다. FixedUpdate에서 하면 문제가 해결되기는 하지만, 뒤에서 설명하는 것처럼 끊김이 발생할 수 있어서 우선은 Update에서 수행하고 있습니다. 나중에 기기 테스트 하면서 보정을 하려고 생각하고 있습니다.
마지막으로, 두둥실 떠 있는 느낌을 주기 위해서 물체가 위아래로 움직이도록 했습니다. AnimationCurve를 노출해서 두둥실의 느낌을 설정할 수 있게 하고, 움직이는 폭은 카메라와의 거리에 따라 조절되도록 했습니다. 안 그러면 물체가 카메라와 가까워졌을 때 너무 많이 움직이게 됩니다.

이동 시 장애물 피하기

장애물을 탐지에도 SphereCast를 사용합니다. 장애물 앞쪽으로 물체가 오도록 카메라와의 거리를 조절합니다.
장애물 피하기

물체를 붙일 수 있는지 판단하기

다시 한 번 SphereCast를 통해서 물체를 붙일 수 있는지 판단합니다. 이 때는 물체에 DroppablePlace 컴포넌트가 붙어있는지 추가로 확인합니다. DroppablePlace 컴포넌트는 물체의 트랜스폼을 조절하기 위한 코드와 설정이 들어있습니다. 예를 들어, 물체가 붙을 때의 로컬 포지션과 로테이션 설정을 가지고 있고, 붙어있는 상태가 바뀔때 호출되는 이벤트도 가지고 있습니다. (이 이벤트를 활용해서 물고기에 물체가 붙었다 떨어질때마다 물고기의 애니메이션을 껐다 켰다 합니다.)

물체를 붙였다 떼기

물체를 붙였다 뗄 때는 DroppablePlace 컴포넌트에 물체의 소유권을 넘겨주는 방식으로 구현했습니다. DroppablPlace 쪽에서 자유롭게 물체의 위치와 회전을 만질 수 있도록 설계를 했고, 뗄 때는 다시 소유권을 돌려받아서 기존의 물체 이동 코드가 위치와 회전을 관리하도록 했습니다.

성능 이슈

마지막으로 최적화와 관련하여 첨언을 하면, 기본적으로 물체의 상태나 종류별로 다른 레이어를 사용합니다. 물체는 처음에 Mob이나 Item 레어어였다가, 이동 중에는 GrabbedItem으로 바뀝니다. 장애물은 Mob, Item, Background 등이고, 붙일 수 있는 곳은 Droppable 레이어 입니다. Raycast를 할 때는 상황에 맞추어 꼭 필요한 레이어만 마스킹해서 실행을 하고, 불필요한 Raycast도 수행하지 않도록 꼼꼼하게 확인합니다. 예를 들어 잡는 중이 아니라면 장애물이나 붙일 수 있는 곳을 검사할 필요가 없고, 어딘가에 붙어 있다면 장애물이 있는지 검사할 필요가 없습니다.
또, 프로젝트 생성 시 기본 물리프레임은 50인 반면에 업데이트/렌더링 프레임은 성능에 따라 60프레임까지 나올 수 있어서 물리 프레임에서 Raycast와 물체 이동을 수행해서 성능 향상을 꾀해볼 수도 있겠지만, 실제로는 렌더링 프레임과 물체의 이동 프레임이 맞지 않으면 움직이다 말다 하는 것이 보여서 부자연스러운 결과가 나옵니다.
어쨌든 아직 제대로 프로파일링해본 것이 아니라서 이후에 최적화를 위해 수정되는 부분이 있을 것 같습니다. 중요한 최적화 포인트가 발생하면 다시 공유하겠습니다.

렌더링 방식

렌더링에서 얘기할 만한 것은 아래와 같습니다.
휘어지는 레이저의 구현
레이저의 끝을 물체의 외곽과 일치시키는 방법
레이저 셰이더 구현
비주얼 이펙트 그래프의 스케일
선택 이펙트
하나씩 살펴보겠습니다.

휘어지는 레이저의 구현

앞에서 잡아끄는 손맛을 위해서 이동을 지연시켰는데, 그 지연된 차이를 베지어 곡선을 사용해서 연결했습니다. 베지어 곡선은 아래와 같이 최소 4개의 제어 포인트를 사용해서 곡선을 만드는 방식입니다.
베지어 곡선
그랩 시스템에서는 P0와 P3를 각각 카메라와 물체의 위치로 잡고, P1을 원래 물체가 있어야 할 곳의 절반 위치 정도로 잡았습니다. P2는 P3와 같게 잡았습니다.
베지어 곡선을 활용한 휘어지는 레이저
현재는 10개의 포인트를 사용해서 위의 베지어 곡선을 표현하고 있고, LineRenderer를 사용해서 그리고 있습니다. 베지어 곡선은 이곳의 코드를 수정해서 사용했습니다.

레이저의 끝을 물체의 외곽과 일치시키는 방법

레이저와 물체의 접점을 보면 동그란 이펙트가 하나 붙어있습니다. 이 이펙트를 어색하지 않게 보여주려면 가능한 한 정확하게 접점의 위치를 파악해야 합니다. 레이저가 요리조리 휘다 보니 어림짐작으로 오프셋 값만 주어서는 어색한 상황이 연출됩니다.
처음에는 물체의 중앙점을 베지어 곡선의 끝(P3)으로 사용하다 보니 정확하게 이 지점을 찾는 것이 어려웠습니다. (심각하게 들어가면 베지어 곡선과 메시 콜라이더와의 충돌 점을 찾아야만 하는..)
그래서 처음에 물체를 잡기 위해서 Raycast 했을 때 히트된 위치를 베지어 곡선의 끝(P3)으로 사용하도록 바꿨습니다. 마치 히트 포인트가 물체의 피벗인 것처럼 다뤘습니다. 이를 위해서 이 위치를 피벗으로 하는 부모 트랜스폼을 하나 만들고 붙잡힌 물체를 자식으로 넣었습니다. 굳이 트랜스폼을 만들지 않고 매번 계산에 포함하는 방법도 있지만, 그랩 시스템 외부와 소통할 때는 순수하게 트랜스폼을 알려주는 것이 간단해서 그렇게 설계했습니다.
히트 포인트를 피벗으로 사용
레이저 셰이더 구현
레이저 셰이더는 이곳을 참고해서 만들었습니다. 기본적으로 UV 애니메이션이고, 다른 속도로 애니메이션되는 노이즈를 섞어주어서 UV 패턴이 잘 보이지 않도록 하는 방식입니다. 아래의 셰이더 그래프로 구현했습니다.
레이저 셰이더 그래프
라인렌더러에서 만드는 버텍스 칼라를 활용하려고 Sprite Unlit Master를 사용하기는 했는데 HDR 범위의 색상을 반환할 수 없는 느낌입니다. 좀 밝게 하려고 ColorScale 파라미터를 넣어서 올려보았더니 밝은 부분이 검은색으로 나와서 우선은 Saturate로 막아두었습니다. 나중에 HDR 범위의 색상을 반환할 수 있게 하고 Bloom을 적용할 계획입니다.

비주얼 이펙트 그래프의 스케일

레이저와 물체가 만나는 부분의 이펙트는 기본 파티클 스프라이트를 사용한 아주 간단한 이펙트입니다. 이번에 처음으로 비주얼 이펙트 그래프를 써봤는데, 폰에 올려서 보니 에디터보다 10배 정도 크게 보이는 문제가 있었습니다.
아직 제대로 공부해 본 적이 없어서 뭔가 간단한 실수를 한 걸 수도 있을 것 같지만, 인터넷을 찾아봐도 별다른 얘기가 없어서 우선은 스케일 파라미터를 노출해서 모바일에서만 1/10 크기로 설정해서 해결했습니다.
비주얼 이펙트 그래프의 스케일

선택 이펙트

물체가 잡혔을 때는 림라이트와 함께 외곽선을 그려주었습니다. 아무래도 각진 큐브 형태의 메시는 림라이트의 효과가 잘 보이지 않아서, 외곽선을 같이 그려주어야 눈에 좀 보이는 편입니다.이런 느낌의 부드러운 Glow 효과를 주고 싶지만, 타일 기반 GPU를 쓰는 모바일 환경에서는 전체 화면을 그리는 부하가 너무 커서 버텍스를 확장하는 방식의 외곽선을 사용했습니다. (예전에 만들어 둔 것도 있고..) Universal Render Pipeline에서 외곽선을 그리는 방법에 대해서는 아래의 포스팅을 참고해주세요.

마치며…

게임을 만들다보면 디테일을 살리기 위해서 정신없이 폴리싱을 하는 단계가 오는 것 같습니다. (구현을 하나 더 할지 폴리싱을 조금 더 할지 늘 갈등하지요.) 포스팅에 적은 것 외에도 쫀득한 맛을 살려보려고 이것저것 시도했다가 실패해서 소개도 못 한 것들이 많은데, 그 과정이 힘들고 허무하기는 하지만 조금씩 조금씩 좋아지다가 어느 순간 게임다운 모습이 되는 것을 지켜보는 맛에 자꾸만 그 과정을 반복하는 것 같습니다. 현재 구현도 폴리싱의 손길을 더 주어야 하지만, 우선은 다음 진도를 나가기 위해서 한 번 끊고 가는 수 밖에요.이번 포스트에서 말씀드릴 부분은 이 정도입니다. 다음번에는 실행시간에 글자를 텍스쳐로 만들어서 물고기와 큐브 아이템에 데칼로 붙이는 포스팅과 컴포넌트 설계방향 및 게임오브젝트 관리에 관한 포스팅을 계획하고 있습니다. 트위터페이스북 팔로우 하시면 놓치지 않고 보실 수 있습니다.
마지막으로 AR 환경에서 테스트하는 영상 보면서 마치겠습니다. 긴 글 읽어 주셔서 고맙습니다.