출처 : https://store.steampowered.com/app/1966720/Lethal_Company
요즘 Lethal Company라는 게임을 하고 있다.
해본 사람은 알겠지만 해당 게임에서는 오브젝트가 외곽선을 가지고 있다.
출처 : https://store.steampowered.com/app/1217060/Gunfire_Reborn
이외에도 Gunfire Reborn, Borderlands 시리즈 등 여러 게임에서 외곽선을 활용하고 있다.
외곽선을 활용하면 현실적이진 않지만 3D도 2D의 느낌을 낼 수 있다는 점에서 주로 사용하기도 한다. (또는 독특한 그래픽을 위해서도 사용한다)
외곽선이 적용된 물체의 경우 주목을 이끌어 낼 수 있기에 마인크래프트처럼 발광 효과나 클릭 가능한 물체의 표시 등 필요에 따라 외곽선이 활용되기도 한다.
이번 게임에서는 큐브가 너무 눈에 안 띌 거 같다는 생각과 이후 추가적으로 이루어질 부분에 대한 밑 작업으로 오브젝트에 외곽선을 추가하기로 마음먹었다.
출처 : https://docs.unity3d.com/kr/Packages/com.unity.shadergraph@10.8/manual/index.html
외곽선은 셰이더로 만들 것이고, 셰이더는 셰이더 그래프를 활용해서 만들 것이다.
렌더링 파이프라인을 아는가?
3D 정보를 2D 이미지로 변환하는 과정이다.
셰이더 그래프를 확인해 보면 Vertex와 Fragment를 다룰 수 있는 것을 알 수 있다.
버텍스 셰이더란 정점(Vertex) 정보의 값을 변경시켜서 효과를 줄 수 있는 것이다.
외곽선을 만드는 것에는 여러 방법이 존재할 것이다.
지금의 방법은 간단한 방법을 활용한 것으로 '대마왕의 유니티 URP 셰이더 그래프 스타트업'과 여러 유튜브 영상을 참고한 것이다.
모델 출처 : 대마왕의 유니티 URP 셰이더 그래프 스타트업 (비엘북스) 예제 데이터
일단 2가지 방법의 결과물을 보자면 이렇다.
두 번째 것이 더욱 정교한 결과를 나타내는 것을 볼 수 있다.
첫 번째 것은 크기를 키운 것에 불과하기에 이빨 같은 부분에 정교한 외곽선이 들어가지 않지만 두 번째 것은 노말 방향으로 확장한 것이기에 외곽선을 얻어낼 수 있는 것이다.
이해가 안 된다면 수치를 조정해서 확인할 수 있다.
보통 3D 오브젝트의 경우 뒷면이 보이지 않기에 그리지 않는다.
이를 활용하여 크기를 키운 오브젝트를 뒷면만 그려주고 외곽선을 적용시킬 오브젝트를 그리는 것이다.
즉 뒷면은 앞면을 가리지 않기에 뒷면을 외곽선으로 사용해 주는 것이다.
이 방법은 오브젝트를 2번 그리기에 2Pass라고 부른다고 한다.
셰이더 그래프의 Graph Inspector에서 Render Face를 수정하면 앞면 대신 뒷면이 그려지게 할 수 있다.
첫 번째 셰이더 그래프이다. 주의할 점은 Space는 Object로 두어야 한다.
그리고 Fragment에서 외곽선의 색인 Base Color를 설정해 줘야 한다.
두 번째 셰이더 그래프이다.
완벽할 것 같은 두 번째 셰이더의 문제가 존재한다.
각진 면에서는 선이 끊어져서 보이는 것이다.
'대마왕의 유니티 URP 셰이더 그래프 스타트업'에 따르면 '각진 부분의 버텍스는 여러 개의 버텍스가 존재한다'라는 사실에 의해 각각의 노말을 가진 버텍스를 각각의 노말 방향으로 이동시키기에 이러한 결과가 나온다고 한다.
이러한 문제를 해결하기 위해선 Hard Edge가 아닌 Soft Edge를 사용해야 한다고 한다.
그러는 것보단 간편하게 만들 수 있는 첫 번째 셰이더 그래프를 사용하는 것이 더 나을 것 같아서 첫 번째 셰이더로 진행하기로 결심했다.
3가지의 적용 방법이 있다.
각각의 방법에 대해서 이야기하기 전에 비교를 하기 위해 렌더링 통계 창을 사용하고자 한다.
우측 위에 보이는 것이 렌더링 통계 창이다.
렌더링 통계 창이란 렌더링 정보를 보여주는 창이다.
출처 : https://docs.unity3d.com/kr/2023.2/Manual/RenderingStatistics.html
우리가 비교하고자 하는 값은 Batches와 SetPass Calls이다.
해당 내용에 대한 설명은 유니티 공식 유튜브인 '[유니티 TIPS] 유니티 최적화를 위한 필수 기본기! Batching 방법 소개'에 내용을 기반으로 작성한다.
위 두 개를 알기 위해서 Draw Call이라는 것을 알아야 한다.
드로우 콜은 CPU가 GPU에게 화면을 그리라고 명령하는 것이라고 한다.
CPU는 GPU에게 명령을 내릴 때 Command Buffer를 통하는데 드로우콜말고도 명령을 보낸다. SetPass란 머터리얼, 셰이더와 같은 그래픽 계열의 그룹을 뜻하고 그것을 전달하는 것이 SetPass Call이라고 한다.
검색을 해보면 SetPass Call은 비용이 많이 드는 작업이기에 SetPass Call을 줄이는 것이 좋다는 내용을 찾아볼 수 있다.
Batch란 Draw Call만이 아니라 그래픽을 구성하는 상태의 데이터를 함께 넘기는 것을 배치라고 한다.
넓은 의미의 Draw Call로 알고 있다.
Batching은 여러 개의 Draw Call을 묶어서 한 번에 처리하는 것을 의미한다고 한다.
머터리얼이 달라도 셰이더가 같다면 SetPass Call을 묶어서 처리해 주는 방법이라고 한다.
오브젝트를 모두 지워보니 Batches와 SetPass Calls가 2의 값을 가진다.
기존의 값에서 12하고 2만큼의 차이를 가진다.
빛을 지웠더니 Batches와 SetPass Calls가 8하고 3의 값을 가진다.
즉 오브젝트 6개를 그리는데 6개의 Batches가 늘어났고 1개의 SetPass Calls이 늘어났다.
초반에 상태를 생각해 보면 이후 빛을 추가할 시 똑같이 6개의 Batches가 늘어나고 1개의 SetPass Calls이 늘어난다는 것을 알 수 있다.
오브젝트를 2개 지웠더니 Batches만 줄어들고 SetPass Calls은 그대로인 것을 볼 수 있다. SetPass Calls은 셰이더의 변경과 관련한 값이기 때문이다.
출처 : https://docs.unity3d.com/kr/2018.4/Manual/FrameDebugger.html
프레임 디버거의 경우 드로우 콜을 나누어서 확인할 수 있는 기능이다.
초기의 상태이다.
SRP Batch로 묶여서 처리해서 그런지 큐브를 지우더라도 6단계로 나뉜다.
하지만 빛을 지운 경우에는 MainLightShadow가 사라지고 5단계로 나뉘는 것을 볼 수 있다.
동일한 위치에 오브젝트를 두고 셰이더로 제작한 머터리얼을 적용해 보았다.
오브젝트를 2개 만든다는 점에서 번거로움이 크다. 또한 오브젝트를 이동시키면 동일한 위치로 옮겨주어야 하기에 별로 좋은 방법이라는 생각이 안 든다.
렌더링 통계 창을 보았다.
Batches는 12만큼 SetPass calls는 4만큼 늘어났다.
Batches는 오브젝트가 1개 늘어날 때마다 2만큼 늘어났기에 올바른 값이다. 하지만 SetPass calls은 아무것도 없을 때는 2였고 오브젝트가 생기고 4였기에 4만큼 늘어난 건 이상하다는 생각이 든다.
이전 작업과 비교하면 MainLightShadow에서 1개, DrawOpaqueObjects에서 3개가 늘어났다.
프레임 디버거에 뜨는 사유를 살펴보면 셰이더가 다르다는 이유이다.
오브젝트를 2개 두는 법과 같은 Batches와 SetPass calls를 가지고 있다.
프레임 디버거 또한 같은 사유를 이야기해 준다.
유니티 공식 번역에선 렌더러 기능이라고 한다.
기능이라는 것은 너무 포괄적이라는 개인적인 생각이 든다.
이것은 설명보다는 결과를 보면 이해하기 쉬울 것 같다.
분명 외곽선이 존재하는데 Batches는 6, SetPass calls는 1만 올라갔다.
확인해 보면 RenderObjects가 추가된 것을 확인할 수 있다.
하나의 과정이 추가된 것이다.
아마도 과정이 추가된 것이기에 SetPass calls이 (같은 셰이더이기에) 1개 추가된 것이고 오브젝트의 개수에 해당하는 6개만큼이나 Batches가 오른 것 같다.
유니버설 렌더 파이프라인 에셋에 적용된 유니버셜 랜더러를 찾아가준다.
이후 Add Renderer Feature를 눌러서 Render Objects를 눌러준다.
이후 설정을 해주는데 Layer Mask에는 적용을 원하는 레이어를 골라주면 된다.
Material에 머터리얼을 집어넣어 주면 된다.
Event를 확인해 보면 여러 옵션이 뜬다.
지금 추가하는 이벤트가 언제 발생될 것이냐는 것이다.
'대마왕의 유니티 URP 셰이더 그래프 스타트업'에 따르면 일반적으로 적혀있는 순서대로 위에서부터 아래로 렌더링이 실행된다고 한다.
BeforeRenderingOpaques를 할 경우 아까와는 다르게 DrawOpaqueObjects에 뒤에 존재하는 것을 볼 수 있다.
사전 작업이 완료됐으면 레이어를 설정해 주기만 해도 외곽선이 적용된 것을 볼 수 있을 것이다.
이점이 Renderer Feature의 강점이라고 생각한다. 앞서 두 방법과 달리 레이어를 설정하기만 하면 외곽선을 추가해 주기 때문이다.
만족스러운 결과가 나왔다.
내 경우에는 어차피 간단한 큐브의 형태이기에 간단하게 작성했다. 추후 꾸미는 과정에서 변경될 수도 있지만 일단은 만족한다.
유튜브 또는 책을 찾아보면 프레넬을 사용하여 외곽선을 만들어준다던가 카메라와 가까워지더라도 외곽선의 크기가 일정한다던가 여러 방법으로 만든 외곽선이 존재한다.
찾아보면서 프로젝트에 맞게 만들어보는 것을 추천한다.
이 부분에서 이야기할 건 개인적으로 느낀 점이다.
검색을 해도 제대로 안 나오는 부분이 많아서 곤혹스러웠다.
외곽선을 Add Renderer Feature로 만든 부분이 레이어로 처리하는 것을 제외하고 성능상으론 무엇이 좋은지에 대한 부분이 특히 그랬다.
그래서 직접 테스트해 보며 작성하긴 했지만 Add Renderer Feature가 100퍼센트 이득을 가져다준다는 보장은 못 한다.
즉 추측일 뿐이다.
잘 모르겠는 부분은 '대마왕의 유니티 URP 셰이더 그래프'와 여러 유튜브, 블로그를 필요한 부분 위주로 확인해 보며 배우고 작성하였다.
그래서 셰이더 자체는 빨리 끝났는데 글 작성이 생각보다 꽤 오래 걸렸다.