유니티에서 직접 데칼 프로젝션 구현하기 - Whitmem
유니티에서 직접 데칼 프로젝션 구현하기
게임 개발 및 엔진
2026-01-03 01:38 게시 1ff4eb49e13d9550135a

0
0
53
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
저작물 참고 필자도 해당 내용을 공부하고 있는 입장으로 주로 오픈 문서, 포럼, AI 질의 응답을 활용하여 관련 내용을 공부하였습니다. 따라서 틀린 내용이 있을 수 있습니다. 다만, 타인(AI 등)의 저작물을 침해하지 않도록 순서, 과정, 스크린샷, 코드 등 다른 저작물을 활용 또는 포함하지 않았으며, AI 등 인터넷 자료는 오로지 기술 원리 이해의 목표로만 활용하고, 본 게시글에 언급된 과정, 흐름, 본문, 게시한 코드는 AI의 복붙 없이 모두 필자가 사전에 알고 있는 그래픽스 기술과 인터넷 상에 공개된 개념을 통해 모두 직접 시도하며 직접 작성하였음을 밝힙니다. 부득이하게 어떤 자료를 인용하는 경우는 링크로 출처를 남기고 인용합니다.
유니티 엔진에서는 데칼 기능을 제공한다. 자체 제공되는 데칼 기능을 사용하기전 직접 구현함으로써 데칼 프로젝션이 어떤 과정을 통해 제공되는지 간단하게 알아보기로 한다. 물론 엔진에서 제공하는 데칼 엔진과는 차이가 있겠으나, 우선 빛 처리 등의 일련의 과정을 모두 무시하고 우선 구현 해보는 것을 주요 목표로 한다.
데칼은 다양한 부분에서 사용된다고 한다 총자국, 손자국, 발자국 등 다양한 부분에 데칼을 사용할 수 있기에 엔진에서 당연하게 제공되는 기능이다.
여기서는 Forward 방식으로 구현할 것인데, 이외 Deferred 방식이 존재한다. 쉐이더를 구현하다보면 Forward 방식으로 할지 Deferred 방식으로 구현할지 고민을 하게 되는 경우가 생기는데, Deferred 는 렌더링에 필요한 구성 요소 (색상, 라이트, 노말 등)의 정보들을 버퍼에 담아 나중에 한 번에 처리할 수 있다는 장점이 있다. 투명도(알파 처리) 등의 문제가 있긴 하지만, 앞에서 노말이나 법선 등의 정보가 담긴 버퍼를 수정해주면 빛 연산도 최종적으로 수정되기 때문에 데칼 등의 처리 등에 유용하다. Forward 는 이미 렌더링된 빛 연산위에 다시 덮어씌우는 방식이므로 여러 눈 속임이 필요하다. (이는 렌더링 방식에 따라 다르므로 확답은 하지 못한다.)
원리는 그렇게 어렵지는 않다. 물론 구현 방법이 다양하기 때문에 한 가지로 단언할 수는 없겠지만 나 같은 경우 하나의 큐브 메시 쉐이더 영역에서 그려야할 부분에 그리는 것으로 구현하였다. 즉 위 사진 예시는 하나의 큐브 메시에 데칼이 렌더링되고 있는 모습이다. 메시의 모든 픽셀 부분에 대해서 충돌 되는 영역을 찾아낸 뒤 해당 영역에 그리는 것이다.
겉 부분에서 충돌 되어 그려야하는 메시 지점 부분만 노란색으로 나타내면 위와 같다.
아무튼 큐브 하나를 만들고 이 큐브에 대해서 쉐이더를 하나 만들면 된다. 이 큐브가 그려지기 직전의 깊이 버퍼를 통해 해당 큐브가 그려져야하는 지점에 이미 그려진 또 다른 객체 (벽, 땅, 플레이어 등)을 가져올 수 있다.이즉 데칼으로 사용할 큐브 메시 (앞으로 데칼 메시라고 하겠다.)의 픽셀이 렌더링되는 각 정점의 스크린 X,Y 좌표에서 깊이 버퍼 Z를 가져오면 결과적으로 해당 X,Y 스크린 좌표에 찍어진 배경 객체의 월드 좌표로 변환할 수 있다. 다만 세계에 존재하는 모든 객체에 대해서 정점을 가져올 수 있는 것이 아니라, 카메라에서 보이는 객체의 보이는 지점만 정점 비교를 할 수 있는 것이다. 즉 카메라 앞에 바로 렌더링된 경우 카메라 뒤에 렌더링된 것은 볼 필요도 없다. 데칼보다 카메라에 더 가까이 있는 메시에는 데칼을 그릴 필요가 없기 때문이다.
아무튼, 쉐이더를 만들고, 스크린 UV 좌표계를 구한다. 스크린 UV 좌표를 그대로 출력하면 아래와 같다.
이를 가까이 가서 보면 아래와 같다.
좌측 밑 영역을 보면 어둡고, 오른쪽으로 갈수록 Red, 위로 갈수록 Green 성분이 강해지는 것을 보아 Screen UV 의 시작점이 왼쪽 밑이라는 것을 알 수 있다. 이는 엔진의 함수인 GetNormalizedScreenSpaceUV이가 자동으로 처리해준 것이며, 직접 변경하는 경우 각 엔진 좌표계에 따라 바꾸어주는 작업도 필요하다. 이는 아래에 후술할 예정이다.
아무튼 스크린 좌표가 정상적으로 가져와졌으므로 이를 통해 깊이 버퍼의 색상을 가져오면 된다.
이를 색상으로 표현하면 깊이 버퍼가 출력되는 것을 볼 수 있는데, 가까이 가면 갈수록 빨강의 계열이 강해지는 것을 볼 수 있다. 즉 멀리일 수록 0이고, 가까이 일수록 1이라는 것을 알 수 있다. 이는 엔진의 시스템에 따라서 달라질 수 있기 때문에, UNITY_REVERSED_Z 라는 플래그를 사용해서 분기점을 처리해야 한다. AI 질문...에 의하면 DX 와 같은 최신 그래픽 라이브러리에서는 [0,1] 또는 [1,0] 범위이고, OpenGL의 레거시 버전에서는 [-1,+1]이라고 한다. 엔진이 이를 자동으로 변환해주는 것이다. 따라서 쉐이더를 직접 작성할 때에는 매크로로 경우의 수를 구현해야 한다.
참고로, 위 예시 이미지들에서 이미 데칼이 적용된 모습으로 보이는데, 사실은 그렇지 않다. 큐브에서 표면에 투영된 부분만 색상을 허용했기 때문에 위와 같은 결과가 나온다. 이는 뒤에서 후술한다.
다음으로, 깊이 버퍼를 구했으므로 각 메시의 스크린 UV와 깊이 버퍼를 조합하여, 해당 메시가 그려지기 직전 가지고 있던 깊이 버퍼(즉 이전 그려진 메시)의 World좌표를 알아내는 것을 목표로 한다. 즉 현재 데칼 메시의 표면 정점이 아니라, 데칼 메시에서 렌더링되는 지점에 해당하는 이미 전에 그려진 버퍼의 3차원 정점을 구하는 것을 목표로 하는 것이다.
따라서 x,y 좌표는 -1~1 범위가 되도록 screenUV 를 스케일링 해주면 되고, depthValue 는 위에서 계산한 깊이 값을 그대로 넣으면 된다. 깊이 값은 이미 계산이 [0,1] 또는 [-1,1]로 계산이 끝났기 때문이다.
한 가지 고려할 것이 있는데, 바로 UNITY_UV_STARTS_AT_TOP 이다. 엔진에서 그래픽 라이브러리에 맞춰서 출력하기 위해 텍스처를 뒤집는 작업을 수행한다. (정확히는 텍스처를 뒤집는 것이 아니라 UV 좌표를 뒤집어 버린다.) DX 좌표계와 GL의 좌표계가 다르기 때문이다. 근데 여기서 헷갈리면 안되는 것이 아까 screenUV 를 구한다고 GetNormalizedScreenSpaceUV를 사용했다. GetNormalizedScreenSpaceUV는 입력으로 들어오는 게임 스크린 좌표계 기준 영역을 스크린 UV 영역으로 변환해주는 역할을 한다. UV 로 단순히 바꾸기만 하면 그래픽 라이브러리에 따라서 UV가 뒤집어지기 때문에 엔진의 GetNormalizedScreenSpaceUV함수가 내부적으로 그래픽 라이브러리에 맞게끔, 즉 최종적으로 오른쪽 하단이 0,0 이 되도록 뒤집어 준다. 한편 프로젝션 행렬 역시 그래픽 라이브러리의 좌표계에 맞춰서 y에 -1 곱해주는 연산이 존재한다. (그래픽 라이브러리가 다른 경우에) 그러면 최종적으로 이 프로젝션을 통해서 결과도 뒤집어진 상태여야 한다. 다만 위에서 우리가 GetNormalizedScreenSpaceUV 함수가 우리 눈에 보기 편하도록 뒤집어 준 (즉 좌측 하단이 0,0인 좌표)를 그대로 정점 좌표로 사용했기 때문에 원래 파이프라인의 흐름에 맞도록 다시 거꾸로 뒤집어진 정점 정보를 넣어서 프로젝션의 역행렬을 곱해줘야 뷰 공간에서 정상적인 모습을 향한다. 즉 파이프라인에서 프로젝션을 거쳤을 때 버텍스가 모두 뒤집어져있어야 하는 것이다. 근데 여기서는 우리 눈에 보기 좋은 UV 좌표로 바로 만들었으므로 최종 결과를 다시 뒤집어주고 프로젝션의 역행렬에 넣는 것이다. 그러면 비로소 뷰 공간에서는 정 방향을 향하는 정점이 나온다.
한편 clipPos 의 w 값은 1.0으로 고정하는데, 이는 원근 구현을 위해 w로 나누는 과정이 있기 때문이다. (즉 여기서는 정확히는 clipPos가 아니다. NDC 영역이랄까?) 여기서는 이미 처리된 결과이므로 1.0으로 고정하면 내부에서 역행렬을 통해 다시 새로운 w가 나올 것이고 이를 다시 나눠주면 되는 것으로 보인다.
Projection 행렬을 거친 뒤에는 w를 반드시 나눠줘야 한다. 원근 처리가 진행되기 때문이다.
그리고 View 역행렬을 통해서 World 공간으로 이동한다. 즉 여기까지 수행하면 뒤에 있는 배경 메시 정점에 대해서 월드 좌표를 구할 수 있는 것이다. 현재 데칼 메시 표면의 월드 좌표를 구한 것이 아니다! 데칼 메시를 통해서 해당 뒷편에 그려지는 배경(카메라에 제일 가까운 오브젝트) 정점의 월드 좌표를 구한 것이라 볼 수 있다.
그리고 비로소 World 공간에 있는 한 점을 데칼 메시의 로컬 공간으로 이동할 수 있다. 즉 과정이 헷갈릴 수 있는데, 데칼 메시의 표면에 있던 정점을 데칼 메시의 로컬에 옮긴 것이 아니다. 데칼 메시에 투영되던 뒷 편 또는 카메라 앞에 있는 한 정점의 월드 좌표를 구해서 (즉 다른 객체의 표면 정점이 될 것이다.) 이 표면 정점의 월드 좌표를 구한 것이다. 이 월드 좌표를 구한 것에 대해서 이제야 데칼 메시의 공간으로 이동하는 것이다.
카메라가 존재할 때 데칼은 저런식으로 존재한다.
데칼의 표면을 렌더링하는 단계이지만, 빨간 점의 screenUV 를 사용해 x,y 를 구했고 (카메라 방향의 스크린 좌표계 기준) 이의 깊이 버퍼를 혼합해 데칼의 빨간 좌표로부터 벽에 존재하는 파란 좌표를 구한 것이다. 파란 좌표의 월드 좌표를 구하고 다시 데칼의 내부 좌표계 속으로 옮긴 것이다. 데칼의 내부 좌표계로 옮기는 이유는 데칼에 충돌했는지 계산하기 쉽게 하기 위해서 이다.
위의 판정 범위를 보자. 데칼의 정점 정보를 직접 쉐이더에서 한번에 가져올 수는 없기 때문에 쉐이더 등의 파라메터에 박스 사이즈를 명시할 수 있는 매개 변수를 별도로 받아야 한다.
즉 위의 _CubeSize는 실제 눈에 보이는 렌더링 되는 사이즈가 아니라, 내부 충돌 검사용 사이즈이다. 위 그림으로 보면 판정 범위라고 볼 수 있다. 판정 범위가 회전되면 단순히 x,y,z 축 if문만으로 비교가 어렵다. 계산이 복잡해진다. 따라서 월드 좌표에 있던 벽의 정점을 데칼의 로컬 좌표계로 가져오면 단순히 부피 x,y,z 로 로컬에서 계산할 수 있다.
이렇게 말이다. 게다가 큐브 사이즈를 총 크기 1.0 (즉 한쪽 -0.5, 0.5)으로 고정하면 이 decalSpacePos 는 결과적으로 데칼 메시의 크기, 회전 행렬에 따라 알아서 로컬 사이즈로 크기가 조절되고 회전도 원래 방향으로 조절되기 때문에 판정용 큐브 사이즈는 최초 로컬 사이즈에 한 번만 맞춰두면 에디터에서 크기를 조절해도 상관이 없다. 즉 데칼 메시의 큐브 정점과 판정용 정점을 일치해두면 안에서 행렬이 적용되면서 판정용 정점이 알아서 축소되기 때문이다.
이제 판정 범위에 벗어나는 부분을 모두 discard 해주면 해당 데칼 메시가 충돌하는 지점만 렌더링되는 것을 볼 수 있다.
이 상태에서 넣고자 하는 데칼 텍스처를 렌더링해주면 샘플링해서 렌더링해주면 되는데, 이때 텍스처 UV를 무엇을 넣어야 할까? 고민할 수 있다.
스크린 UV를 넣어서 그대로 렌더링하면 위와 같이 될 수 있고, 그렇다고 메시의 Seam된 UV를 넣으면 메시의 표면이 어색하게 렌더링된다. 데칼 프로젝션의 주요 목표는 해당 데칼이 바라보는 방향으로 그림을 우겨 넣어야 하는 것이다. 즉 데칼 로컬 좌표계 기준 한 방향만 x,y 로 UV를 만들어서 샘플링되도록 구현 해 두면 에디터에서 행렬을 통해 월드 회전을 하면 월드 회전된 각각의 위치에 로컬 좌표계 기준 정면이 그대로 투영되어서 그려질 것이다.
즉 위 그림을 예시로 들자면 데칼 메시는 회전되어 있지만, 데칼 메시의 로컬 좌표계 기준으로 아래가 UV 0, 위가 UV 1이 되도록 하려면, 데칼 메시의 로컬 좌표계에서 X,Y 축을 그대로 가져와서 UV로 써주면 된다. 그러면 데칼의 한 평면이 UV 좌표가 될 것이기 때문에, 그 방향으로 그려질 것이다. x,y 를 방향으로 할지, x,z 를 방향으로 할지, z,y를 방향으로 할지 등은 본인 마음이다. 나는 그냥 표면의 땅을 향하도록 하기 위해 로컬 좌표계 기준 x,z 로 대입하였다. 이때 데칼 메시의 정점 좌표를 그대로 넣는 것이기 때문에 데칼 메시의 로컬 좌표계에서 각 버텍스가 -0.5 ~ 0.5 의 기본 큐브 모양이므로 UV에서는 이것이 0~1이 되도록 +0.5만 더하였다. 데칼 메시의 크기를 월드 행렬에서 늘리더라도 이는 로컬 좌표계 계산이기 때문에 전혀 영향 없이 그대로 스케일링 되어서 데칼이 적용될 것이다.
이렇게 구현된 것을 볼 수 있다. 데칼을 구현하다보면 알겠지만 데칼 메시이 향하는 방향으로 그려진다. 아무리 겹쳐있어도 정방향으로 구현하면 아래와 같이 될 수 있다.
이는 땅의 표면은 한 축에 대해서 사영되는 UV가 100% 똑같기 때문에 늘어지는 현상이 발생하는 것이다. 즉 페인트를 뿌리거나 발자국을 뿌리거나 총을 쏘는 등 데칼 작업을 하려면 해당 데칼 메시의 방향 벡터를 그리고자 하는 방향으로 생성해야 한다. 이제 데칼을 직접 구현했으니 다음에는 엔진에서 제공하는 편리한 데칼 기능을 사용해보고자 한다.
댓글 0개
댓글을 작성하는 경우 댓글 처리 방침에 동의하는 것으로 간주됩니다. 댓글을 작성하면 일회용 인증키가 발급되며, 해당 키를 분실하는 경우 댓글을 제거할 수 없습니다. 댓글을 작성하면 사용자 IP가 영구적으로 기록 및 부분 공개됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.