Unity/Rendering&Shader

[SHADER] Object의 Scale 값을 이용한 UV Shader

김성인 2023. 11. 8. 18:55

[SHADER] Object의 Scale 값을 이용한 UV Shader

 

셰이더 제작보다, 셰이더를 제작하다 문제가 생겼던 부분을 기록하고자 남긴다.


 혼자서 3D + 도트 아트 스타일의 게임을 만들며 비주얼 향상과 개발 속도 향상에 고민을 많이 하게 되었다. 특히 비주얼 향상을 위해서는 한 화면에 걸리는 모든 오브젝트들의 픽셀의 사이즈와 밀도를 균일하게 만드는 것이 관건이었다.

 

 게임을 개발을 진행하다 보니 처음 계획한 것보다 오브젝트들의 크기가 변경하게 되는 경우가 많고, 배경에서는 배치한 오브젝트들의 사이즈를 변경하다 보니 픽셀의 사이즈가 들쑥날쑥하게 되는데, 이 부분을 해소하기 위하여 반복된 수정이 많았고, 오브젝트의 사이즈를 기준에 맞춰서 일정한 단위로 끊어 제작하는 등의 여러 가지 규칙을 세우게 되었다.

 

 그렇게 진행하다 보니 어느 정도 완성이 되지 않는 상태에서는 어떤 결과가 나올지 손에 들린 원화와 머릿속 상상에 맡겨야 했고, 이런저런 니즈의 해결 책으로 오브젝트의 크기 값을 이용하여 UV와 Pixel 사이즈를 자동으로 수정할 수 있다면 많은 부분에서 비용이 줄어들 것이라는 생각이 들었다.


  총 2종류의 셰이더를 제작하게 되었다.

   첫 번째 오브젝트의 사이즈 변경에 맞춰 UV 사이즈를 자동으로 변경해 주는 셰이더

   두 번째 오브젝트의 사이즈 변경에 맞춰 Pixel의 사이즈를 자동으로 변경해 주는 셰이더

 

 이 두 가지 셰이더는 하나의 시작점에서 시작한다. 바로 오브젝트의 사이즈이다. 다행스럽게 유니티에서는 이미 셰이더에서 오브젝트의 사이즈 값을 받아 오는 함수가 존재한다. 그리고 셰이더 그래프 또한 이 노드가 존재한다.


 첫 번째 오브젝트의 사이즈 변경에 맞춰 UV 사이즈를 자동으로 변경해 주는 셰이더를 만들었다.

 

 결과물은 이렇다.

 

 완성된 셰이더 그래프 노드이다.

 

이 노드를 이용하여 오브젝트의 사이즈와 UV를 곱하는 방식으로 손쉽게 첫 번째 셰이더를 제작할 수 있었다.

 

 

 


그리고 이 노드는 유니티 메뉴얼에서 코드를 확인하면 이렇게 되어 있다.

 

이 코드를 이용하여 UV를 변경하기 위하여 아래와 같이 코드를 작성도 해보았다.

 

 물론 이 셰이더의 단점이 존재한다. 바로 3D 오브젝트에는 제대로 적용할 수 없다는 것이다. 정확히는 3D 오브젝트의 Transform은 축이 x, y, z 총 3개이다. 하지만 UV는 축이 x, y 총 2개라는 것이다. 이런 미스매칭으로 인하여 극히 드문 상황에서 적용할 수 있다.


 이제 우리는 오브젝트의 크기를 받아와 셰이더에서 활용할 수 있다는 것을 알게 되었고, 위의 셰이더를 변형하여 진짜 만들고자 했던 두 번째 셰이더를 제작해 보자.

 

 앞전에 포스트 프로세싱을 이용하여 도트 아트 스타일을 만드는 부분을 참고하여 오브젝트의 스케일 값과 연결 해 주면 된다.

 

 하단에 첨부한 gif 이미지를 확인하면 내가 설정한 픽셀 사이즈에 맞춰 오브젝트의 크기를 늘리고 줄이더라도 픽셀의 사이즈는 동일한 결과를 얻는다!

 

 완성된 셰이더 그래프 노드이다.


 우선 픽셀 사이즈를 내가 정할 수 있도록, 오브젝트의 스케일과 픽셀 사이즈를 정하는 변수를 곱해줘야 한다. 곱연산의 특징산 UV에 곱해줘도 되지만, 연산을 한 번만 하기 위하여 이렇게 곱하였다.

 

 이제 나머지는 UV를 최종적으로 제일 우측의 바툭판 모양과 같이 만들기 위함이다. UV 안쪽의 사각형 하나하나가 Pixel의 사이즈가 된다.


 코드로 변경하면 이렇게 된다.

 우선 위의 코드는 코드 상으로 문제도 없고, 수식 또한 맞다. 하지만 이 코드는 내가 의도한 방향으로 작동하지 않았다.

 

 위 코드를 보면 OUT.uv 가 존재한다. 이것으로 위 코드는 버텍스 셰이더라는 것을 유추할 수 있는데, 이 부분이 바로 함정이었다. 결론부터 말하면 프래그먼트 셰이더에서 계산하게 되면 정상적으로 작동한다.

 

 몇 번이고 내가 작성한 코드를 확인하였지만 문제가 없었다. 그리고 결과물을 확인하며 _PixelScale 변수 값을 바꿔가며 디버깅을 시작하였고, 이 변수 값을 바꿀 때 특이점을 발견하였다.

 

 픽셀이 삼각형의 모습으로 일그러지는 것이었다. 이 삼각형을 보고 생각할 수 있는 것은, 모델 파일은 버텍스에 데이터들을 담는 것이고, 버텍스 칼라 등을 저장 후, 버텍스와 버텍스 사이의 값을 보간하여 우리에게 보여 주는 방식이다.

 

 그렇기 때문에 프래그먼트 셰이더에서 계산하게 되면 정상적으로 나올 것 같았고, 코드를 수정하였다. 이렇게 하여 정상 작동하는 것을 확인하였다.

 

 왜 이럴까? 우선 floor 함수를 거치기 전까지는 문제가 없다. floor를 거치게 되면 문제가 생기는데 정확한 이유는 시간상 정확한 원인은 파악 못했지만 의심되는 것은 버텍스 안에 저장할 수 있는 메모리의 양(대역폭이라고도 할 수 있겠다)이 정해져 있다는 것이다. 이를 뒷받침해주는 실험으로 버텍스를 많이 사용하여 만든 경우, 버텍스 셰이더에서 계산을 하여도 의도한 방향으로 정상 작동을 하는 것을 확인하였다.

 

 버텍스 수에 따라 이미지가 어떻게 변화하는지 하단에 첨부한 gif 이미지를 보자.

 왼쪽부터 오른쪽으로 갈수록 버텍스의 수를 늘렸다. 이 테스트를 보면 버텍스가 많을수록 이미지에 정상 작동하며, 아티팩트가 줄어드는 것을 확인할 수 있다.

 

 확대하면 이와 같이 아티팩트의 크기가 작아지는 것을 볼 수 있다.

 이와 같은 이슈가 있을 때는 텍스처의 Pixel 개수와 동일한(정확히는 하나 더) 수의 버텍스 수가 있어야 의도한 결과물을 얻을 수 있다. 그렇기 때문에 최적화를 위하여 처음부터 프레그먼트 셰이더에서 계산을 진행하면 좋을 것 같다.


 참고로 터레인에서 하이트 맵 레졸루션을 보면, 32, 64… 2승의 텍스처를 넣는데, 레졸루션 값이 +1씩 되는 것은 텍스처의 픽셀 데이터를 이용하여 터레인의 버텍스 데이터의 높이 값을 변형하기 때문인 것 같다. 이 부분에서 조금 더 자세히 설명한다면 픽셀은 하나의 픽셀로 존재하지만, 버텍스의 경우 좌, 우 꼭짓점을 가져야 안쪽에 색을 채우고 보여줄 수 있는 것과 같은 이치이다.


 번외 편 - 그럴싸한 무언가... 전혀 원했던 것은 아니지만 재미있는 엉뚱한 결과물이다.


사용된 도트 이미지는 ToYou님께서 제작하였습니다.