Unreal/Rendering&Shader

[Post-Processing] Pixelized

김성인 2023. 8. 9. 01:50

[Post-Processing] Pixelized

 

 

- 목적 : 후보정 기능을 이용하여, 빠르고 간단하게 게임 비주얼을 Pixel Art로 변환하기 위함.

 

- 원리 : 화면의 UV의 정밀도를 떨어트려, 화면의 해상도를 줄이는 듯한 시각효과를 이용함.

 

- UV의 정밀도를 떨어트린다?
 UV는 2차원 벡터로 (0, 0)에서 (1, 1)까지로 표현할 수 있고, 이 사이에는 아주 많은 수들이 들어 있다. 여기서 아주 많은 수들의 "단위"를 줄이게 되면 어떻게 될까? 예를 들어서 0.1112223라는 임의의 수가 있다면, 이 수를 소수점 1의 자리 밑의 값은 날려 버린다면 앞의 수는 0.1이 된다. 이것은 0에서 1사이에 존재하는 수많은 수를 10개로 줄이는데, 100개 1000개를 사용하던 수를 10개로 줄이는 것을 UV의 정밀도를 떨어트린다고 생각하면 쉬울 것 같다. 사실 이것은 프로그래밍적으로 보면 숫자를 저장하는 파일의 용량을 줄여, 보통 소수점 이하의 단위를 어쩔 수 없이 줄어드는 것이지만

 

- UV의 정밀도를 떨어트리면 이렇게 된다.
 앞의 이야기를 그래프화하면, 직선의 경사로가 계단과 같이 된다. 

 UV를 이미지화하면 하단의 이미지를 얻을 수 있고, 좌측의 이미지와 같이 표현된다.  그리고 이 사각형 하나하나가 바로 Pixel을 이루는 최소한의 단위가 된다.

 

UNITY 2022 LTS / Shader Graph  14.0.8에서 작성

주의사항 : Shader Graph의 Scene Color 노드를 사용하지 않는다. Blit Buffer 사용.

 

 Shader Graph로 새로운 Shader를 만들고, Graph Settings의 Material을 Fullscreen으로 변경해 준다.

 위와 같이 Fullscreen으로 변경하면, Shader Graph의 최종 출력 노드인 마스터 스택이 아래와 같이 바뀌게 된다.

 UV는 0 ~ 1의 수로 존재하고, 이것을 임의의 수로 곱하게 되면, 0부터 임의의 수로 값의 범위가 늘어나게 된다. 그리고 이것을 Floor 등의 함수로 소수점을 버리게 되면, 정수만 남게 된다.


 예를 들어서 UV에 5를 곱하게 되면, UV의 범위 값이 0 ~ 1에서 0 ~ 5가 되고, 0에서 5까지 안에는 아주 많은 수들이 존재한다. 예를 들어 0.00001 같은 것들인데, Floor 함수를 이용하여 정수만 남기게 하면, UV는 0, 1, 2, 3, 4, 5의 값만 남게 된다.

 

 여기까지를 이미지화하게 되면 UV는 하단의 이미지가 되는데, 흰 부분이 너무 많다. 분명 0부터 5까지 단계가 나뉘어 표현되어야 하는데, 생각했던 것과 다르게 나온다.

 이것은 UV는 0에서 1까지 표현되는데, 1을 넘은 값들이 모두 1과 같이 보이기 때문이다.  그럼 위에서 구한 값에 다시 임의 수로 나누어 주게 된다면, 1을 넘어 간 값들을 1의 안쪽으로 돌려놓을 수 있게 된다. 

 

 여기까지를 이미지화하면 하단의 이미지가 나온다.

 이것을 수식화 하면 Floor(UV * x) / x가 된다.

 

 Shader Graph는 이렇게 된다.

위의 노드에 Scene Color라는 렌더링 된 화면 정보를 불러오는 노드를 연결해 주면 끝날 것 같이 보인다.

위와 같이 Shader를 만든 후, Material을 만들어 주고, Edit/Project Settings/Graphics/ScriptableRenderPipelineSettings/Rendering/Renderer List의 Renderer Features에 제작한 Material을 연결해 주면 끝이 난다.

 하지만 위에서 말함 임의의 수를 바꾸다 보면, 두 가지 문제점을 발견할 수 있다.

 

 Pixelized 되었지만, Pixel이 정사각형이 아니라는 것과 화면의 상단과 우측의 Pixel은 다른 것들과 사이즈가 다르게 나온다.

 다시 Shader Graph로 돌아가서, 이 두 부분을 수정해 보자.

 

 끝부분 Pixel의 사이즈까지 모두 균일하게 만들기 위하여, 입력하는 임의의 수 또한 Floor 함수를 이용하여 정수만 나오도록 하면 된다. 

 하지만 Pixel을 정사각형으로 만들기 위하여, 화면의 사이즈 값을 임의의 수로 나누어 적용하게 되면, Pixel은 다시 정사각형에서 직사각형으로 바뀌기 때문에, 이 부분 뒤에 Floor 함수를 이용하면 해결할 수 있다.

 

 게임 플레이의 화면은 보통 가로, 세로의 해상도가 다르다.  그렇기 때문에, 가로, 세로 구성하는 Pixel의 수가 같다면 어쩔 수 없이 Pixel은 정사각형이 깨지기 마련이다.  그래서 화면의 가로, 세로 사이즈 값을 얻어, UV의 각축을 해상도에 맞춰서 계산해 줘야 한다.

 위까지 진행하면 최종적으로 이런 노드가 완성 된다.


 여기까지 정리하고, 위에서 Shader Graph로 제작한 Shader를 Material에 연결한다. 그다음 이 Material을 Universal Renederer Data에 "Add Renderer Feature" 기능을 이용하여, Full Screen Pass Renderer Feature를 추가한다.

 

 이제 문제가 없어 보일 것이다. 단, 내가 Scene에 배치한 오브젝트들이 물과 같이 투명하지 않다면 말이다.

 하단의 이미지를 두 장을 보면 투명한 오브젝트는 제대로 렌더링 되지 않는 것을 확인할 수 있다.

 투명한 오브젝트인 물만 Pixelized 되지 않았거나,

 투명한 물이 아예 렌더링 되지 않는다.

 

 왜 이럴까? 왜 Transparent 오브젝트만 렌더링 되지 않을까? 렌더링 과정을 떠올린다면 쉽다. Opauqe와 Transparent는 따로 계산한다.

 

 그리고 Universal Renederer Data의 Injection Point 보면,  "Before Rendering Transparents" 등을 선택할 수 있고, 이것은 렌더링 하는 과정 중에 내가 만든 효과를 끼어 넣는 위치를 정하는 것이라고 생각하면 좋겠다.

 

 위의 사실이 맞는지 Unity의 프레임 디버거를 열어 확인해 보면, 내가 지정한 순서에 맞도록 렌더링 되고 있다.

 렌더링 하는 과정은 보통 메모리에 데이터를 올리고, 계산 후, 다시 메모리에 올리고를 반복하며 차곡차곡 쌓기 때문에, Transparents 다음에 계산한다면, 당연히 포함되어 계산되어야 하는데 이상하다.

 

 방금 렌더링의 과정은 메모리에 올리고, 메모리에 올리고라는 말을 사용하였는데, 렌더링 과정 중에 메모리를 비우지 않는 이상, 한번 계산된 데이터는 메모리에 남아 있다. 그렇다면 FullScreenPassRendererFeature은 계산 전에 어디서 이미지(데이터)를 가져오는지 프레임 디버거에서 확인해 보니, CopyColor에서 가져오는 것이었다.

 

 

 CopyColor는 Distortion을 효과를 만들 때, 사용되던 그랩 패스라고 생각하자.  참고로 URP부터는 그랩 패스가 빠졌다.

 그리고 렌더 타겟이 _CameraOpaqueTexture이다... "O.p.a.q.u.e"... CopyColor가 이루어진 시점에서 Opaque만 카메라가 찍은 것이다. 또 여기서 문제는 ColpyColor는 Injection Point와 상관없이 DrawTranspaterntObjects 전에 이루어진다.

이 CopyColor를 보면 렌더 타겟이 CameraOpaqueTexture라고 나온다.

 다시 한번 프레임 디버거에서 내가 만든 셰이더가 무슨 이미지(데이터)를 이용하는지 확인하기 위하여 FullScreenPassRendererFeature를 보자. 그럼 "_CameraOpaqueTexture"라는 것을 확인할 수 있다.

 Scene Color 노드는 CopyColor를 참조하기 때문에 Renderer Data에서 렌더링 순서를 바꾸어도 되지 않는다.

 

 위의 이유 때문에 처음부터 Shader Graph의 Scene Color 노드를 이용하지 않는다고 한 것이다.

 

 CameraOpaqueTexture 대신 blitTexture를 이용하기로 하였고, 이것을 이용하는 노드가 URP Sample Buffer이다. Scene Color 노드를 대신하여 이 노드를 연결해 주고 Source Buffer에 "BlitSource"를 연결해 주면 완성이다.


 Post Processing을 이용하여 Pixel Art 비주얼을 만들 경우, 실제 해상도가 떨어진 것이 아니기 때문에, FPS가 증가하지 않는다.  그렇기 때문에, 무작정 이 방법을 이용하여 게임을 만드는 것보다 Pixel 사이즈를 생각하여 저 해상도로 텍스쳐링을 진행하고, UI가 문제 되지 않는 선에 한하여 물리적으로 해상도를 낮추길 바란다.


 프레임 디버거에서 확인할 수 있었던, CopyColor 드로우콜의 경우 URP Asset에서 Opaque Texture를 활성 해 주어야만 한다.

 만약 Opaque Texture를 활성 하지 않아, 버퍼를 생성하지 않았다면, Shader Graph의 Scene Color 노드가 제대로 작동하지 않고, 작업을 진행할 버퍼가 없기 때문에, 무슨 짓을 해도 화면이 바뀌지 않는다. 그리고 화면만 변하지 않을 뿐, 실제로는 렌더링 되고 있다... 단지 FullScreenPassRendererFeature가 앞에서 진행된 렌더링 과정을 덮어 버린다고 생각하면 쉽게 이해할 수 있을 것 같다.

 

 여기서 Opaque Texture는 스냅샷을 이용하여 렌더링 과정 중에 여러 효과들에 이용할 수 버퍼이다.  당연하게도 사용 시 메모리를 차지하게 된다.


 위의 Pixelized를 적용했는데, Pixel의 경계 부분에 아티팩트가 생긴다면 흔하게 Downsampling이 원인이다. 그 외에 가끔 LOD가 문제가 되기도 한다.

 이 경우 Opaque Downsampling을 꺼버리면 해결된다. 이렇게 해도 되지 않는다면, Graphics Quality의 Anisotropic Textures가 Per Texture로 되어 있지 않다면, 이 옵션으로 선택하면 해결된다.


 UV를 Base Color로 출력했을 때, 녹색, 노랑, 빨강으로 나오지 않는 경우는 UV의 모든 채널을 Base Color로 출력하기 때문이다.

 UV를 Split으로 나누어 RG 채널만 이용해야 우리가 흔하게 시각화 한 UV를 볼 수 있다.