Search

Universal RP로 구현한 툰 셰이딩

date
2019/09/18
tags
아들과 2인개발
SRP
URP
렌더링
유니티3D
4 more properties
아들과 2인 개발 모음
Search
안녕하세요! 이번에는 Universal Render Pipeline(이하 URP)을 사용한 툰 셰이딩에 대해 얘기해볼까 합니다. 원래는 아들이 그린 원화 느낌을 살리기 위해서 색연필 셰이딩을 해보려고 했지만, 이런저런 벽에 부딪히다가 정신을 차리고 보니 유니티 2019.3 베타 버전과 URP, Renderer Features라고 하는 실험적인 기능으로 툰 셰이딩을 구현하게 되었습니다. 이 글에서는 삽질의 결과물을 마치 원래 알고 있었던 것처럼 자연스럽게 정리해보겠습니다. 완성된 셰이더부터 잠깐 구경하시죠~
완성된 셰이더

툰 셰이딩

셰이딩 알고리즘은 이 글을 참조했습니다. 디퓨즈, 하이라이트, 림라이트를 두 단계로 층이 지게 만드는 것이 기본 아이디어입니다.
툰 셰이딩 기본 아이디어
노멀 벡터와 라이트 벡터를 dot 프로덕트 해서 표면의 밝기 구한 후에 smoothstep을 사용해 두 단계의 밝기로 나눕니다. 아래 셰이더 그래프에서 smoothstep 부분을 보시면 이해가 될 겁니다. 하이라이트와 림 라이트도 같은 방식을 사용해 두 단계로 나눕니다.
smoothstep으로 명부와 암부를 명확하게 나눔
추가로 림 라이트가 전체적으로 나오면 외곽선의 느낌이 들기 때문에 밝은 면에만 림 라이트가 나오도록 합니다. 아래 그래프를 보시면 표면의 밝기와 림 라이트를 곱해주고 있습니다.
밝은 표면에만 림 라이트가 보이게
이 셰이딩 알고리즘을 셰이더 그래프로 옮기는 과정에서는 두 가지 난관이 있었습니다. 그 중 첫 번째는 라이트 정보를 가져오는 노드가 없다는 것인데요, 다행히도 최근에 추가된 커스텀 함수 노드를 사용해서 HLSL 코드를 셰이더 그래프에 임베딩 할 수가 있었습니다. 자세한 방법은 유니티 블로그를 참조하시면 됩니다. 아래가 그 커스텀 함수 노드입니다.
라이트 정보를 얻어오는 커스텀 함수 노드
또 하나의 난관은 어떤 마스터 노드를 쓰느냐! 였습니다. URP에서는 PBR 마스터와 Unlit 마스터 중의 하나를 골라서 사용해야 하는데 두 가지 모두 단점이 있습니다. 최종적으로 Unlit 마스터를 고르기는 했지만 다른 물체의 그림자를 받을 수 없다는 단점이 있습니다. 위의 커스텀 함수에서 Shadow Attenuation을 받아오기는 하지만, 실제로는 셰도우 맵 자체가 전달되지 않습니다.
Unlit 마스터
PBR 마스터를 쓸 때는 Albedo에 0을 주고 Emission에 커스텀 라이팅 결과를 넣어줘야 하는데, 아무리 Albedo가 0이라도 라이팅에 영향을 주게 됩니다. 아직은 프로토타이핑 단계라서 그냥 넘어가지만, 나중에는 문제가 될 수도 있을 것 같습니다.
셰이더 그래프를 쓰면서 가장 좋았던 것은 중간 과정의 프리뷰를 볼 수 있다는 점입니다. 실제 게임 상황이 아니라 프리뷰라는 한계가 있지만, 기존 방식보다 훨씬 효율적으로 문제를 찾을 수 있었습니다. 반면에 연산의 결과를 여기저기서 가져다 쓸 때는 어떤 노드에서 선을 그어야 할지 헷갈렸습니다. 자꾸 쓰다 보면 익숙해질 것 같기는 합니다.

아웃라인

아웃라인을 그리는 방식은 크게 포스트 프로세싱을 사용하는 방식과 버텍스를 확대해서 한 번 더 그려주는 방식이 있습니다. 아무래도 포스트 프로세싱은 프래그먼트 연산이 많아져서 꺼려지기도 했고 원하는 오브젝트만 아웃라인을 그리고 싶어서 버텍스를 확대하는 방식을 선택했습니다. (참고로, 이 동영상을 보시면 브롤 스타즈에서는 캐릭터를 렌더 텍스쳐에 그린 후에 텍스쳐 상에서 아웃라인을 만든다고 합니다.)
셰이딩 알고리즘은 이 글을 참조했습니다. 기본적인 아이디어는 이렇습니다.
Object Space가 아닌 Clip Space에서 버텍스를 노멀 방향으로 옮겨준다. (2D 상에서 작업하는 효과)
카메라와의 거리에 따라서 외곽선의 굵기가 변하는 것을 보정하기 위해서 Clip Space 좌표의 w 컴포넌트를 활용한다. (카메라에서 멀수록 w 컴포넌트의 값이 커짐)
화면 비율에 대응하기 위해서 _ScreenParams를 활용한다.
아래는 위의 알고리즘을 구현한 버텍스 셰이더 코드입니다.
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, input.normalOS)); float2 offset = normalize(clipNormal.xy) / _ScreenParams.xy * _Outline * vertexInput.positionCS.w * 2; output.positionCS = vertexInput.positionCS; output.positionCS.xy += offset;
C
이렇게 노멀 벡터 방향으로 버텍스를 밀어주게 되면, 큐브와 같이 모서리의 노멀이 서로 다른 방향을 바라보는 경우에 외곽선이 끊어지는 현상이 발생합니다. 아래 그림에서 오른쪽 큐브의 모서리를 보면 됩니다.
왼쪽: 외곽선을 부드럽게 모델링한 큐브, 오른쪽: 유니티 기본 큐브 (모서리 부분 외곽선에 빈틈)
해결책으로는 외곽선을 부드럽게 모델링을 하거나 외곽선 용의 노멀을 버텍스의 별도 채널에 저장해두는 방법이 있습니다. 관련해서는 다음 번 포스트에서 설명합니다.
이 셰이딩 알고리즘을 기존의 빌트인 파이프라인에서 구현한다면 셰이더에 아웃라인용 패스만 살짝 추가하면 됩니다. 하지만 URP는 멀티 패스로 그리는 것이 간단하지가 않아서 인터넷을 검색하면 멀티 패스와 관련된 문의가 엄청나게 많은 것을 볼 수 있습니다.
Scriptable Render Pipeline(이하 SRP. URP도 SRP 기반으로 만들어진 것)를 밑단부터 사용해서 직접 렌더링 파이프라인을 만든다면 여러 가지 대안이 있겠지만, URP를 사용할 때는 커스텀 패스를 추가하는 방법이 제일 적절해 보였습니다. 특히 유니티 2019.3에는 Render Features라는 것이 생겨서 코딩 없이도 커스텀 패스를 추가할 수 있게 되었습니다! (이것이 바로 유니티 2019.3 베타로 업그레이드한 이유입니다.)
관련하여 유니티가 GDC 2019에서 세션을 발표했고, 깃허브에 예제도 올려두었습니다. 포스트 프로세싱 방식과 버텍스 확장 방식 모두 포함되어 있으니 참고하시면 좋을 것 같습니다. 예제에서는 Object Space에서 버텍스를 밀어주는 간단한 알고리즘을 사용합니다.
Render Features를 사용해서 아웃라인 패스를 추가하는 방법은 이렇습니다. 우선, UniversalRenderPipelineAsset을 하나 만들어서 Graphics 설정에 지정해 줍니다.
그리고 렌더러 애셋을 만들어서 위의 파이프라인 애셋에 지정해 줍니다.
마지막으로 렌더러 애셋에는 Render Features에 Render Objects 항목을 추가합니다. Layer Mask에 Mixed.. 이라고 나오는데, 외곽선을 그려줄 오브젝트의 레이어를 선택해주면 됩니다. 그리고 아웃라인용 재질을 지정해줍니다. 이렇게 하면 해당 레이어의 오브젝트들만 Outline 재질을 사용해서 한 번 더 그리게 됩니다.
Event를 보면 After Rendering Opaques로 되어 있는데, 불투명한 오브젝트를 그린 후에 그리라는 뜻입니다. 프레임 디버거로 보면 실제로 그렇게 하고 있습니다.
Renderer Features를 사용해서 외곽선을 그리면 셰이더만 교체하는 것이 아니라 재질을 교체하는 것이라서 오브젝트별로 재질의 프로퍼티를 바꾸는 것은 불가능합니다. 반면에 같은 재질로 그리기 때문에 배칭이 되어서 드로우콜을 줄일 수 있는 장점이 있습니다. (오브젝트별로 외곽선의 두께나 색을 바꾸고 싶다면 버텍스의 별도 채널에 정보를 넣어두는 방식을 사용할 수 있습니다.)
(수정) Renderer Features를 사용해도 같은 재질을 그대로 사용하면서 셰이더 패스만 바꿔줄 수 있습니다. 아래 포스트를 참고하세요.
여기에 링크!
(추가) 전체 셰이더 그래프를 보고 싶다는 분들이 계셔서 이미지로 올려보려고 했는데, 전체 그래프가 한 눈에 보이면서 가독성도 확보하는게 어렵네요. 그래서 github에 올려두었으니 공부하실 분들은 참고하시기 바랍니다. (https://github.com/mycodingdad/URPToonShader)

마치며..

이번 포스트는 여기까지 입니다. 새 기능들을 실험해 보느라 쉬운 일을 어렵게 해낸 느낌입니다.
가능한 코딩없이 구현해 보려고 노력해봤는데, 이번과 같이 빠르게 프로토타입을 해볼 때는 유용할 것 같습니다.이번 셰이딩 결과를 AD님(아드님)께 보여줬더니 흔쾌히 컨펌해줬습니다. 요괴메카드로 신나게 놀던 중이라서 자세히 보기는 한 것인지 모르겠네요..
긴 글 읽어 주셔서 고맙습니다.