Unity 검색

유니버설 렌더 파이프라인으로 확장 가능한 고성능 그래픽스 구현

2020년 2월 10일 엔진 & 플랫폼 | 15 분 소요
다루는 주제
공유

Is this article helpful for you?

Thank you for your feedback!

유니버설 렌더 파이프라인은 콘텐츠 제작을 위한 여러 아티스트 도구를 포함하는 탁월한 솔루션입니다. 뛰어난 화질과 성능으로 모든 Unity 플랫폼에서 지원되는 게임을 제작하려면 유니버설 렌더 파이프라인을 사용해 보세요. 유니버설 렌더 파이프라인의 다양한 장점은 지난 블로그 포스팅에서 다루었습니다. 이번 포스팅에서는 유니버설 렌더 파이프라인을 사용하여 보트 어택(Boat Attack) 데모를 제작한 방법을 살펴보겠습니다.

유니티는 유니버설 렌더 파이프라인(제작 당시에는 ‘경량 렌더 파이프라인’)을 테스트하기 위해 보트 어택 데모를 제작했습니다. 개발 과정에서 버티컬 슬라이스 데모를 제작하여 실제 개발 과정을 테스트해 보려는 목적이었습니다.

보트 어택 데모는 제작 이후로 꾸준하게 업그레이드되었으며, 현재는 유니버설 렌더 파이프라인의 여러 새로운 그래픽 기능을 비롯하여 C# 잡 시스템, 버스트 컴파일러, 셰이더 그래프, 입력 시스템 등 최신 Unity 기능을 사용합니다.

지금 바로 보트 어택 데모를 다운로드하여 Unity 2019.3에서 사용할 수 있습니다.

데모

보트 어택은 보트 레이싱 게임의 소규모 버티컬 슬라이스 데모입니다. 지금 바로 플레이할 수 있으며, 최신 Unity 기능을 최대한 활용하기 위해 계속해서 업데이트하고 있습니다.

이 데모는 중간 사양 및 고사양 모바일 기기, 모든 최신 콘솔 및 스탠드얼론 앱 등 다양한 플랫폼에서 원활하게 작동합니다. 유나이트 코펜하겐 2019에서는 iPhone 7부터 PlayStation 4에 이르는 다양한 기기에서 보트 어택 데모를 시연했습니다.

데모를 사용하려면 Unity 2019.3의 최신 버전을 설치하고 GitHub에서 프로젝트를 다운로드하면 됩니다. readme 파일에서 사용 지침을 반드시 확인하세요.

셰이더 그래프

셰이더 그래프는 셰이더 제작에 사용되는 아티스트 친화적 인터페이스로, 테크니컬 아티스트를 위한 강력한 프로토타이핑 툴입니다. 유니티는 셰이더 그래프를 사용하여 보트 어택 데모의 일부 셰이딩 효과를 구현했습니다.

셰이더 그래프는 탁월한 셰이딩 효과를 구현하는 데 유용하며, 다양한 버전의 경량 렌더 파이프라인 및 유니버설 렌더 파이프라인에 걸쳐 손쉽게 관리할 수 있습니다.

보트 어택의 암벽 셰이더는 메시 데이터를 사용하여 얻을 수 있는 효과를 보여줍니다. 셰이더 그래프에서는 메시로부터 손쉽게 데이터를 얻을 수 있습니다. 메시의 노멀 벡터를 사용하여 암벽에 붙은 평평하고 위로 향하는 이끼를 표현하고, 메시의 월드 공간 높이를 사용하여 수면과 가까운 암벽에는 이끼를 부착하지 않았습니다.

왼쪽부터: Y 높이 마스크, Y 노멀 마스크, 높이 + 리매핑된 노멀 마스크, 최종 셰이더.

초목 셰이딩

보트 어택의 초목은 원래 커스텀 버텍스/프래그먼트 셰이더였지만, 렌더 파이프라인 개발 초기에는 유지 관리가 까다로웠고 코드도 자주 변경되었습니다. 반면 셰이더 그래프를 이용하여 셰이더를 재생성하면 업그레이드가 수월합니다.

셰이더 그래프 효과는 Tiago Sousa of Crytek에서 사용한 방식을 기반으로 구현했는데, 이 프로젝트에서는 버텍스 변위를 통해 버텍스 컬러를 적절히 활용하여 바람에 흔들리는 애니메이션을 제어했습니다. 보트 어택에서는 바람 효과를 계산하는 데 필요한 모든 노드를 포괄하는 하위 그래프를 만들었습니다. 이 하위 그래프는 반복적인 계산을 수행하는 유틸리티 그래프의 집합체인 중첩된 하위 그래프를 포함합니다.

개별 버텍스 애니메이션 마스크. 왼쪽부터: 버텍스 컬러의 적색 채널에는 원점까지의 거리에 따라 잎을 포함한 나무 전체가 휘는 정도(main bending), 녹색 채널은 잎사귀의 세부 움직임(phase offset), 그리고 청색 채널에는 나뭇가지(branch) 움직임에 대한 정보를 저장.

사실적인 초목을 구현하기 위해서는 SSS(Subsurface Scattering, 피하 산란)도 매우 중요하지만, 현재 유니버설 렌더 파이프라인에서는 사용할 수 없습니다. 하지만 셰이더 그래프의 커스텀 함수 노드를 사용하면 유니버설 렌더 파이프라인에서 조명 정보를 가져와 SSS와 유사한 효과를 구현할 수 있습니다.

노드 레이아웃. SSS 마스크는 그린 버텍스 컬러(잎사귀)와 알베도 텍스처 맵으로 만들어집니다.

커스텀 함수 노드를 활용하여 다양한 효과를 자유롭게 연출할 수 있습니다. 여기에서 커스텀 렌더링 기법을 자세히 알아보세요. 보트 어택 저장소에서 노드 코드를 가져와 조명을 원하는 대로 테스트해볼 수도 있습니다.

왼쪽부터: SSS 사용 안 함, SSS만 사용함, 최종 셰이더.

보트 커스터마이징

보트에 다양한 컬러를 적용할 수 있도록 서브스턴스 페인터를 이용하여 두 가지 패턴 마스크를 입히고 메탈릭(적색), 평활도(녹색), 패턴 1(청색) 및 패턴 2(알파) 항목을 포함하는 텍스처 패키지로 저장했습니다. 셰이더 그래프의 마스크를 사용하여 마스크 영역에 컬러를 선택적으로 적용했습니다.

보트에 컬러가 적용되는 원리입니다. 오버레이 블렌딩을 사용하면 베이스 알베도 맵을 통해 미세한 컬러를 표현할 수 있습니다.
셰이더 그래프의 노드 레이아웃. 부모 RaceBoats 그래프에서 손쉽게 사용할 수 있도록 하위 그래프에 포함되어 있습니다.

주택 건물

보트 어택은 낮과 밤의 풍경을 모두 담고 있습니다. 시간의 변화를 더욱 사실적으로 표현하기 위해 레벨 전체에 걸쳐 건물 창문의 셰이더 그래프를 만들었습니다. 이 셰이더 그래프는 해질녘에는 창문을 비추는 조명을 켜고 동이 틀 무렵에는 조명을 끕니다.

낮/밤 값에 매핑된 간단한 이미션 텍스처를 통해 이를 구현했습니다. 그리고 오브젝트 위치를 사용하여 조명을 켜고 끄는 순서를 무작위화하여 각각의 집에 설치된 조명이 간격을 두고 점등되도록 했습니다.

무작위 이미션을 설정한 노드 맵.

구름

보트 어택에 역동적인 조명을 추가했으니 이제 단순한 HDR 이미징 스카이박스보다는 씬 조명에 따라 다이내믹한 방식으로 구름을 구현할 차례입니다.

하지만 큰 뭉게구름을 실시간으로 렌더링하기에는 부담이 큽니다. 특히 모바일 하드웨어에서 실행하기는 더 힘듭니다. 구름을 여러 각도에서 볼 필요는 없으므로, 텍스처를 포함하는 카드를 사용하여 성능 요구치를 줄이기로 결정했습니다.

구름을 렌더링하는 전체 흐름 그래프.

셰이더 그래프는 룩을 프로토타이핑하는 데 핵심적인 역할을 했습니다. Houdini에서 일부 볼류메트릭 구름 데이터를 베이크하고, 셰이더 그래프에서 커스텀 조명을 구현했습니다. 이 구름은 좀더 개선의 여지가 있지만, 노드 기반 에디터를 사용하여 다양한 표면을 만들어낼 수 있음을 입증했다는 점에서 의미가 있습니다.

API 렌더링을 통해 자연스러운 평면 반사 구현

유니티는 스크립터블 렌더 파이프라인을 이용하여 렌더링 코드를 숨기지 않고 사용자가 커스터마이즈할 수 있도록 공개하고자 합니다. 따라서 단순히 기존의 렌더링 코드를 공개하기보다는 새로운 API 및 하드웨어를 염두에 두고 렌더링 기술을 발전시켰습니다.

유니버설 렌더 파이프라인을 사용하면 C# 언어를 사용하여 즉각적으로 렌더링을 진행할 수 있습니다. 이 파이프라인은 다음의 4가지 후크 함수를 노출합니다.

  • RenderPipelineManager.beginFrameRendering
  • RenderPipelineManager.beginCameraRendering
  • RenderPipelineManager.endCameraRendering
  • RenderPipelineManager.endFrameRendering

후크를 사용하면 씬이나 특정 카메라를 렌더링하기 전에 자체 코드를 손쉽게 실행할 수 있습니다. 보트 어택에서는 후크를 사용하여 메인 프레임이 렌더링되기 이전에 씬을 텍스처로 렌더링하여 평면 반사를 구현했습니다.


private void OnEnable()
{
   RenderPipelineManager.beginCameraRendering += ExecutePlanarReflections;
}

이 코드는 콜백을 구독(subscribe)하기 때문에 OnDisable에서는 구독을 해지합니다.

여기에서 평면 반사 스크립트의 엔트리 포인트를 확인할 수 있습니다. 이 코드를 사용하면 유니버설 렌더 파이프라인이 카메라를 렌더링할 때마다 커스텀 메서드를 호출할 수 있습니다. 이 메서드의 이름은 ExecutePlanarReflections 입니다.


public void ExecutePlanarReflections(ScriptableRenderContext context, Camera camera)
{
    //rendering code....
}

[beginCameraRendering] 콜백을 사용하기 때문에, 메서드에서 [ScriptableRenderContext] 및 [Camera]를 파라미터로 사용해야 합니다. 이러한 데이터는 콜백을 거쳐 전달되며, 렌더링될 카메라를 알려줍니다.

이 코드는 카메라와 행렬을 다룬다는 점에서 평면 반사를 구현하는 데 일반적으로 사용하는 코드와 대부분 동일합니다. 유일한 차이점은 유니버설 렌더 파이프라인이 카메라 렌더링을 위한 새로운 API를 제공한다는 점입니다.

평면 반사를 구현하는 전체 메서드는 다음과 같습니다.


private void ExecutePlanarReflections(ScriptableRenderContext context, Camera camera)
{
   // we dont want to render planar reflections in reflections or previews
   if (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview)
       return;

   UpdateReflectionCamera(camera); // create reflected camera
   PlanarReflectionTexture(camera); // create and assign RenderTexture

   var data = new PlanarReflectionSettingData(); // save quality settings and lower them for the planar reflections

   beginPlanarReflections?.Invoke(context, m_ReflectionCamera); // callback Action for PlanarReflection
   UniversalRenderPipeline.RenderSingleCamera(context, m_ReflectionCamera); // render planar reflections

   data.Restore(); // restore the quality settings
   Shader.SetGlobalTexture(planarReflectionTextureID, m_ReflectionTexture); // Assign texture to water shader
}

여기에서는 주어진 카메라를 렌더링하기 위해 새로운 [UniversalRenderPipeline.RenderSingleCamera()] 메서드를 사용합니다. 이 경우 카메라는 평면 반사 카메라입니다.

이 카메라는 설정한 대로 [Camera.targetTexture]를 사용하여 텍스처를 렌더링하므로, 이후 렌더링 시에 물 셰이딩에 사용할 수 있는 RenderTexture를 얻게 됩니다. GitHub 페이지에서 전체 PlanarReflection 스크립트를 확인해 보시기 바랍니다.

평면 반사 구성. 왼쪽부터: 원본 평면 반사 카메라 출력 결과, 프레넬 감광 및 노멀 오프셋, 최종 물 셰이더, 평면 반사가 적용되지 않은 물 셰이더.

데모에서 위 콜백은 일부 렌더링을 호출하기 위해 사용되었지만, 다른 목적으로도 사용할 수 있습니다. 예를 들어 평면 반사 카메라에서 그림자를 비활성화하는 데 사용할 수도 있고, 카메라에 사용할 렌더러를 선택하는 데 사용할 수도 있습니다. API를 사용하면 씬 또는 프리팹 내 동작을 하드 코딩하는 대신 더욱 복잡한 기능을 세부적으로 제어할 수 있습니다.

특수 효과에 커스텀 렌더 패스 삽입

유니버설 렌더 파이프라인에서는 ScriptableRenderPass를 기반으로 렌더링이 이루어집니다. 이번에는 렌더링 대상 및 방법에 대한 지침에 대해 알아봅시다. 여러 ScriptableRenderPass를 한 번에 요청하여 ScriptableRenderer를 생성합니다.

유니버설 렌더 파이프라인의 또 다른 부분은 ScriptableRendererFeature입니다. 커스텀 ScriptableRenderPass를 위한 데이터 컨테이너이며, 첨부된 모든 유형의 데이터와 더불어 패스를 무제한으로 포함할 수 있습니다.

또한 ForwardRenderer 및 2DRenderer라는 두 가지 ScriptableRenderer가 있습니다. ForwardRenderer는 ScriptableRendererFeature 추가를 지원합니다.

ScriptableRendererFeature를 더욱 쉽게 생성하기 위해 C# MonoBehaviour 스크립트와 유사하게 템플릿 파일로 시작할 수 있는 기능을 추가했습니다. 프로젝트 뷰를 마우스 오른쪽 버튼으로 클릭한 다음 [Create/Rendering/Universal Pipeline/Renderer Feature] 중에서 선택하면 템플릿이 생성됩니다. 템플릿이 생성되면 ForwardRendererData 에셋의 Render Feature 목록에 ScriptableRendererFeature를 추가할 수 있습니다.

보트 어택 데모에서는 ScriptableRendererFeature를 사용하여 물결 무늬(caustics)와 물 효과(WaterEffects)를 위한 두 가지 렌더링 패스를 추가했습니다.

물결 무늬

물결 무늬용 ScriptableRendererFeature는 씬 전체에 걸쳐 Opaque 및 Transparent 패스 간에 커스텀 물결 무늬 셰이더를 렌더링하는 패스를 추가합니다. 하늘에 있는 모든 픽셀을 렌더링하는 것을 방지하기 위해 물과 나란히 있는 큰 사각형을 렌더링하게 됩니다. 이 사각형은 카메라를 따라가지만 수면 높이에 맞추어 스냅되며, 불투명한 패스에서 화면에 표시되는 항목에 셰이더가 가산적으로 렌더링됩니다.

물결 무늬 렌더 패스 합성. 왼쪽부터: 뎁스 텍스처, 뎁스로부터 월드 공간 위치 재구성, 월드 공간 위치에 매핑된 물결 무늬 텍스처, 불투명 패스로 최종 블렌딩된 모습.

[CommandBuffer.DrawMesh]를 사용하여 사각형을 그리고, 메시의 위치를 지정하기 위해 행렬을 만든 후(물 및 카메라 좌표 기반), 물결 무늬 머티리얼을 설정할 수 있습니다. 코드는 다음과 같습니다.


public class WaterCausticsPass : ScriptableRenderPass
{
   const string k_RenderWaterCausticsTag = "Render Water Caustics";
   public Material m_WaterCausticMaterial;
   public Mesh m_mesh;

   public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
   {
       var cam = renderingData.cameraData.camera;
       if(cam.cameraType == CameraType.Preview) // Stop the pass rendering in the preview
           return;

       // Create the matrix to position the caustics mesh.
       Vector3 position = cam.transform.position;
       position.y = 0; // TODO should read a global 'water height' variable.
       Matrix4x4 matrix = Matrix4x4.TRS(position, Quaternion.identity, Vector3.one);

       // Setup the CommandBuffer and draw the mesh with the caustic material and matrix
       CommandBuffer cmd = CommandBufferPool.Get(k_RenderWaterCausticsTag);
       cmd.DrawMesh(m_mesh, matrix    , m_WaterCausticMaterial, 0, 0);
       context.ExecuteCommandBuffer(cmd);
       CommandBufferPool.Release(cmd);
   }
}

물 효과

WaterFXPass가 적용된 모습/적용되지 않은 모습. 왼쪽: 최종 렌더링 결과물, 오른쪽: 물에 패스의 결과물만 보여주는 디버그 뷰.

WaterFXPass는 조금 더 복잡합니다. 이 효과의 목표는 오브젝트가 물에 영향을 주어 파도 및 거품 등을 일으키게 하는 것이었습니다. 이러한 효과를 위해 텍스처의 각 채널에 서로 다른 정보를 작성할 수 있는 커스텀 셰이더를 사용하여 특정 오브젝트를 오프스크린 RenderTexture로 렌더링합니다. 즉, 거품 마스크는 적색 채널에, 노멀 오프셋 X 및 Z는 녹색 및 청색 채널에, 물의 움직임은 알파 채널에 렌더링됩니다.

WaterFXPass 합성. 왼쪽부터: 최종 결과물, 월드 공간 노멀을 생성하기 위한 녹색 및 청색 채널, 거품 마스크에 사용되는 적색 채널, 물의 이동을 만들어내기 위한 알파 채널(적색: 포지티브, 흑색: 변동 없음, 청색: 네거티브).

먼저, 렌더링할 텍스처를 반해상도로 준비합니다. 그런 다음, WaterFX 셰이더 패스가 있는 투명한 오브젝트에 필터를 생성합니다. 마지막으로 [ScriptableRenderContext.DrawRenderers]를 사용하여 오브젝트를 씬에 렌더링합니다. 최종 코드는 다음과 같습니다.


class WaterFXPass : ScriptableRenderPass
{
   const string k_RenderWaterFXTag = "Render Water FX";
   private readonly ShaderTagId m_WaterFXShaderTag = new ShaderTagId("WaterFX");
   private readonly Color m_ClearColor = new Color(0.0f, 0.5f, 0.5f, 0.5f); //r = foam mask, g = normal.x, b = normal.z, a = displacement
   private FilteringSettings m_FilteringSettings;
   RenderTargetHandle m_WaterFX = RenderTargetHandle.CameraTarget;

   public WaterFXPass()
   {
       m_WaterFX.Init("_WaterFXMap");
       // only wanting to render transparent objects
       m_FilteringSettings = new FilteringSettings(RenderQueueRange.transparent);
   }

   // Calling Configure since we are wanting to render into a RenderTexture and control cleat
   public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
   {
       // no need for a depth buffer
       cameraTextureDescriptor.depthBufferBits = 0;
       // Half resolution
       cameraTextureDescriptor.width /= 2;
       cameraTextureDescriptor.height /= 2;
       // default format TODO research usefulness of HDR format
       cameraTextureDescriptor.colorFormat = RenderTextureFormat.Default;
       // get a temp RT for rendering into
       cmd.GetTemporaryRT(m_WaterFX.id, cameraTextureDescriptor, FilterMode.Bilinear);
       ConfigureTarget(m_WaterFX.Identifier());
       // clear the screen with a specific color for the packed data
       ConfigureClear(ClearFlag.Color, m_ClearColor);
   }

   public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
   {
       CommandBuffer cmd = CommandBufferPool.Get(k_RenderWaterFXTag);
       using (new ProfilingSample(cmd, k_RenderWaterFXTag)) // makes sure we have profiling ability
       {
           context.ExecuteCommandBuffer(cmd);
           cmd.Clear();

           // here we choose renderers based off the "WaterFX" shader pass and also sort back to front
           var drawSettings = CreateDrawingSettings(m_WaterFXShaderTag, ref renderingData,
               SortingCriteria.CommonTransparent);

           // draw all the renderers matching the rules we setup
           context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);
       }
       context.ExecuteCommandBuffer(cmd);
       CommandBufferPool.Release(cmd);
   }

   public override void FrameCleanup(CommandBuffer cmd)
   {
       // since the texture is used within the single cameras use we need to cleanup the RT afterwards
       cmd.ReleaseTemporaryRT(m_WaterFX.id);
   }
}

두 ScriptableRenderPass 모두 ScriptableRendererFeature에 포함됩니다. 이 클래스는 리소스를 설정하고 UI를 통해 설정을 전달할 수 있는 [Create()] 함수를 포함합니다. 물을 렌더링할 때는 이러한 요소가 항상 함께 사용되기 때문에 한번에 ForwardRendererData에 추가할 수 있습니다. Github에서 전체 코드를 확인할 수 있습니다.

향후 계획

19.4 LTS가 포함된 Unity 2019 주기에 걸쳐 이 프로젝트를 계속해서 업데이트할 계획입니다. Unity 2020.1부터는 프로젝트가 원활히 실행되도록 유지하되 새로운 기능은 추가되지 않을 예정입니다.

다음은 향후 계획의 일부입니다.

  • 낮/밤 표현 완성(커스터마이징의 필요성을 줄이기 위해 유니버설 렌더 파이프라인에 더 많은 기능 통합)
  • 물 UX/UI 세부 조정
  • 임포스터 적용
  • 코드 클린업 및 성능 개선

유용한 링크

보트 어택 GitHub 저장소

전체 2019.3 프로젝트 링크(GitHub을 사용하지 않는 경우)

유니버설 렌더 파이프라인 매뉴얼

 

유니버설 렌더 파이프라인과 고해상도 렌더 파이프라인

유니버설 렌더 파이프라인은 HDRP(High Definition Render Pipeline, 고해상도 렌더 파이프라인)를 대체하거나 포함하지 않습니다.

유니버설 렌더 파이프라인은 한 번만 개발하면 어디에든 배포할 수 있는 Unity의 기본 렌더 파이프라인으로 활용될 예정입니다. 이 파이프라인은 더욱 유연하고 확장 가능하며, 빌트인 렌더 파이프라인에 비해 더욱 높은 성능을 구현하고 여러 플랫폼에 걸쳐 확장할 수 있습니다. 그래픽 품질 또한 매우 탁월합니다.

HDRP는 하이엔드 플랫폼에서 최신 그래픽스를 제공합니다. 하이엔드 하드웨어 수준으로 그래픽스 품질을 향상하고 고성능의 비주얼을 구현하려면 HDRP가 가장 적합합니다.

프로젝트의 특징 및 플랫폼 요구 사항에 따라 렌더 파이프라인을 선택하도록 합니다.

유니버설 렌더 파이프라인 시작하기

지금 바로 유니버셜 렌더 파이프라인을 제작에 사용할 수 있습니다. 업그레이드 툴을 사용하여 프로젝트를 업그레이드하거나, Unity Hub의 유니버설 프로젝트 템플릿을 이용하여 새로운 프로젝트를 만들어 보세요.

유니버설 렌더 파이프라인 포럼을 통해 피드백을 보내 주시기 바랍니다.

2020년 2월 10일 엔진 & 플랫폼 | 15 분 소요

Is this article helpful for you?

Thank you for your feedback!

다루는 주제