0.8 C
Seoul
Sunday, April 5, 2020
Home Blog 2 패스 셰이더 포팅

[URP 포팅] 2 패스 셰이더 포팅

URP 포팅 시리즈의 4번째 포스트 입니다. 지난 번에는 커스텀 라이팅 헤어 셰이더를 URP용 HLSL로 포팅하는 과정을 기록했습니다. 이번에는 헤어 셰이더가 2 패스로 구현된 부분을 포팅하는 기록입니다. 작업 기록을 옮긴 것이라 중간에 실수하고 착각하는 내용도 있습니다. 오해의 여지가 있는 부분은 괄호 안에 첨언했습니다.


이 셰이더는 알파테스트와 알파 블렌딩 두 개의 패스로 구현된 셰이더인데, URP에서는 멀티 패스 셰이더를 지원하지 않는다. 방법 중의 하나는 Renderer Features 기능을 사용해서 URP 렌더러에 끼워 넣는 방식. RenderObjects를 사용하면 C# 코딩을 안하고도 가능한 방법이 있다.

Renderer Features와 RenderObjects란?

Renderer Features란 URP 렌더러를 확장할 수 있는 기능입니다. ScriptableRendererFeature 클래스를 상속받아서 나만의 Renderer Feature를 만들어 렌더링 패스를 끼워 넣을 수 있습니다. RenderObjects란 URP에서 미리 만들어 놓은 Renderer Feature 중에 하나로 원하는 오브젝트를 다른 재질을 사용해서 다시 그릴 수 있는 기능입니다.

RendererObjects를 사용하면 추가 패스를 어느 시점에 끼워 넣을지 고를 수 있다.

어떤 오브젝트를 다시 그릴지는 렌더 큐, 레이어, 셰이더 패스 이름으로 필터링 할 수 있다. 예를 들어, 모든 몹에 대해서 아웃라인을 그린다든지 할 때는 몹 레이어로 필터링을 하면 되는 식. 근데, 내 경우에는 멀티 패스 렌더링을 위해서 레이어를 하나 소비하고 싶지는 않다. 그렇다면, 큐와 패스 필터만으로 어떻게 할 수 있을까? 자세한 문서를 못 찾겠다. 코드를 뒤져보자.

RenderObject.cs 안에 ScriptableRendererFeature를 상속받은 RenderObject 클래스가 있다. 모든 필터링 설정은 ScriptableRenderPass를 상속받은 RenderObjectsPass 클래스로 넘어간다. 그 안에서 렌더 큐와 레이어 마스크는 UnityEngine.Rendering.FilteringSettings에 보관되고,  패스는 UnityEngine.Rendering.DrawingSettings로 넘어간다. 그 두 가지 모두 ScriptableRenderContext.DrawRenderers() 로 넘겨진다. 소스 코드가 공개 안된 부분이라서 더 이상 추적 불가. 매뉴얼을 보자.

지정한 패스만 그리게 되니 필터의 역할이 맞다. 해당 패스만 그리되 설정해둔 Override Material을 사용해서 그리는 것 같다.  ForwardLit 이라고 되어 있던 Pass 이름을 ForwardLitHair로 바꿔보자. 렌더링은 잘된다.  (잘 되는 것처럼 보이지만 사실은 아니었습니다. 이유는 뒤에서… 🤫)

Render Objects를 끼워 넣어보자. 아래처럼 URP 렌더러에 추가.

오버라이드용 재질을 하나 만들고 Pass Index는 1로 지정. 원래 셰이더의 두 번째 패스로 알파 블렌딩 패스를 끼워 넣을 예정.

이렇게 재질을 오버라이드하는 방식은 셰이더 프로퍼티도 모두 덮어 써야 하기 때문에, 프로퍼티는 그대로 쓰면서 패스만 더해주고 싶을 때는 불편하다. 헤어 재질도 여러 개가 될 수 있으므로 그때마다 렌더러 설정을 바꿔주는 코딩도 해야 하고. 우선 돌아가는지만 보고 다른 해결책을 찾아야겠다.

오.. 예상대로는 잘 돌아간다. 근데 알파 블렌딩되는 부분이 이상하다. 분명히 불투명 오브젝트를 다 그리고 난 후에 그리라고 설정했는데(After Rendering Opaques) 터레인이 그려지기 전에 알파 블렌딩 패스가 그려진다. RenderObjects 사용법 중에 모르는 것이 있나 보다.

RenderObjects의 Event를 이리저리 바꿔줘도 효과가 없다. 잘못 이해한 건가. 소스 체크 고고. 

RenderObjects 클래스에서 RenderObjectsPass 클래스로 넘겨진다. 이 패스는 ScriptableRenderer.EnqueuePass()에 전달되고 실행될 때 SortStable() 함수 안에서 정렬이 된다.  ScriptableRenderPass 클래스에는 <, > 오퍼레이터가 오버로딩되어있고, renderPassEvent 기준으로 반환한다. 그렇다면, 위에 넣어준 Event에 맞게 정렬이 되야할 것 같은데 이상하다.

SortStable()에 브레이크포인트를 걸어보니 잘 걸린다. 정렬이 잘 되나 보자.

정렬은 잘되어 있고, 그 순서대로 RenderObjectsPass.Execute() 는 호출된다. 그 안에서 ScriptableRenderContext에 DrawRenderers()나 ExecuteCommandbuffer()를 호출한다. 근데, 왜 순서가 안맞을까?

컥, RenderObjects 설정을 없애도 알파 블렌딩 패스가 그려진다. 🤭그렇다. RenderObjects는 동작하고 있지 않았고, 그냥 셰이더의 2패스가 연속으로 그려졌던 것. 알파블렌딩 패스에는 Tag도 없는데 왜 그려졌을까?

스펙인지 버그인지 모르겠지만 셰이더에서 첫 번째 패스에 Tags {“LightMode” = “UniversalForward”}로 하고 두 번째 패스에 Tags를 지우니 두 개의 연속된 드로우콜로 그려진다. 두 번째 패스에 Tags를 다시 넣으면 첫 번째 패스만 그려진다. 혹시나 하고 두 패스 모두 Tags를 지우니 마찬가지로 첫 번째 패스만 그려진다.
프레임 디버거에 이렇게 멀티 패스 셰이더라고 언급하는 걸 보면 단순한 버그는 아닌 것 같다.

불투명한 오브젝트를 그리는 ScriptableRenderPass를 디버깅해보자. ShaderTagIdList에는 예상대로만 들어 있다. (ShaderTagIdList에 포함되는 태그와 일치하는 셰이더 패스를 가진 셰이더를 가진 재질을 가진 렌더러를 가진 오브젝트들만 그려지는 것이 원래 스펙입니다. 👻)

공개된 소스코드 안에서는 왜 태그가 없는 패스도 같이 그려지는지는 알 수가 없다. 증상만 정리하면 이렇다.

  • 두 패스 모두 UniversalForward 태그가 있으면 첫 번째 패스만 그려진다
  • 두 패스 모두 태그가 없으면 첫번째 패스만 그려진다
  • 두 패스 모두 UniversalForward가 아닌 이상한 이름의 태그를 지정하면 아무것도 안 그려진다
  • 한 패스가 UniversalForward 태그를 가지고 다른 패스가 태그가 없으면 둘 다 그려진다. 패스 순서에 상관없이 태그가 붙은 패스 먼저 그려짐.
    • 이 때 태그 없는 패스가 하나 더 있어도 그려지지는 않는다
  • 두 패스 모두 UniversalForward 태그가 있으면 첫 번째 패스만 그려진다

어쨌든 우리 셰이더는 불투명에서 한 번, 투명에서 한 번 그려줘야 하므로 이 기능(혹은 버그)를 사용할 수 없다.

그러니, Render Objects 기능이 왜 동작을 안 하는지 파악해서 돌아가게 하자. 확인해 보니 문제의 원인은 Filters에 잘못된 값을 넣었기 때문. 일단 Queue는 Opaque를 넣어야 한다. 기본적으로 URP에 의해서 렌더링되는 첫번째 패스에 대한 정보를 넣어줘야 된다. 그러면 URP가 다시 해당 오브젝트를 렌더링하면서 이번에는 오버라이드된 재질로 그리는 방식이니까.

또 한 가지 잘못한 것은 Shader Passes에 패스의 Name을 넣었는데, Tags의 LightMode 값을 넣어야 한다. 아래의 코드를 예를 들면 “StandardLit”이 아니라 “UniversalForward”를 넣어야 한다.

 Pass
        {
            Name "StandardLit" 
            Tags{"LightMode" = "UniversalForward"}

결론적으로 Render Features는 내 용도에 맞는 물건이 아니다. (용도에 맞는 물건이 맞아요. 제정신이 아니었나 봐요. 😓 설명은 뒤에..) 우선 패스의 Name으로 필터가 안되기 때문에 헤어 게임오브젝트만 덧그릴 방법이 없다. 그리고 재질 애셋을 넣어주는 방식이라서 재질이 런타임에 변경되어서 Instanced로 되어 버리면 첫 번째 패스는 Instanced 된 재질을 쓰고, 두 번째 패스는 재질 애셋을 쓰게 되어서 셰이더 파라메터들이 달라지는 문제가 있다.

코딩을 좀 하더라도 가능한 방법을 찾아보자. 결국에는 FilteringSettings에 의해서 무엇을 그릴지 결정되는데 이 안에는 RenderingLayerMask가 있다.


이는 그냥 Layer와 다른 것으로 MeshRenderer에 보면 지정할 수 있게 되어 있다.

우선 필터링은 해결. RenderingLayerMask를 써서 헤어용 레이어를 하나 할당하면 될 것 같다. (결국에는 다른 방법을 씁니다. 😓 설명은 뒤에..)

재질이 분리되는 이슈는 재질은 오버라이드 하지 않고 패스만 오버라이드 할 수 있으면 해결된다. DrawSettings에 재질 오버라이드 프로퍼티와 패스 오버라이드 프로퍼티가 분리되어 있기는 한데 재질을 오버라이드 하지 않아도 작동할지는 해봐야 안다.


이 두 가지가 된다면 OK. 우선 패스만 오버라이드 가능한지 테스트하기 위해서 URP 코드를 살짝 바꿔보자.
RenderObjectPass.Execute()에서 아래 줄을 주석.

//drawingSettings.overrideMaterial = overrideMaterial; 


안된다 ㅠ 재질을 오버라이드해야만 패스를 바꿀 수 있다. 아.. 캐릭터가 한 명만 나오면 인스턴스화 된 재질을 찾아서 직접 넣어줄 텐데 여러 캐릭터가 나올 거라. 재질을 오버라이드하지 않고 패스만 오버라이드할 방법이 필요하다.

아! 생각의 방향이 잘못되었다. (이제야 깨달음을 얻었습니다. 🦸‍♂️ 이제부터가 최종 버전입니다.) 그냥 필터 설정에서 패스 이름을 UniversalForward가 아닌, 두 번째 패스의  LightMode 값을 넣어주면, 해당 패스로 그리게 된다.

셰이더에서 두 번째 패스에 태그를 이렇게 주고 Tags {“LightMode” = “HairAlphablending”}
RenderObjects의 Shader Passes를 이렇게 주면 된다.

이렇게 하면 HairAlphablending이라는 LightMode 태그를 가진 셰이더 패스만 그려지게 되므로, 별도로 RenderingLayerMask 같은 것으로 필터링 할 필요도 없다. 헤어 셰이더만 HairAlphablending 패스를 가질테니까.

이렇게 해서 같은 셰이더 프로퍼티를 사용해서 불투명 패스, 투명 패스를 그리는 셰이더 구현 성공! 👍

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay connected

58FansLike
56FollowersFollow
156FollowersFollow
128FollowersFollow

Recipe of the day