0.8 C
Seoul
Sunday, April 5, 2020
Home Learn Tutorials #1 커스텀 렌더 파이프라인 (1/2)

[Catlike Coding의 Custom SRP] #1 커스텀 렌더 파이프라인 (1/2)

제가 정말로 좋아하는 Catlike Coding의 Jasper Flick님이라고 아시나요? 이 분의 렌더링 튜토리얼은 정말 제가 본 중에 최고라고 말할 수 있습니다. 저 혼자만 알고 싶은 마음이 들다가도, 혼자만 알고 있기에는 아까운 마음이 드는 명저 중의 명저입니다. 이 분이 요새 Custom SRP 튜토리얼을 연재 중입니다. 역시나 차근차근 기초부터 설명하는 친절함이 가득한 튜토리얼입니다. 그런데 이렇게 좋은 글을 소개해드려도 영어 부담에 주저하시는 분들이 계십니다. 그래서 저도 꼼꼼하게 공부할 겸 SRP 연재를 우리말로 간단히 옮겨보려고 합니다. 제대로 된 번역은 너무 힘들고 어려워서 그냥 말은 통하는 정도로 옮길 예정이니 관대한 마음으로 읽어주세요. 👌

그런데 Jasper Flick님이 제 트위터 팔로워라죠? 😎 Jasper님도 팔로우 하는 제 트위터 주소는 여기입니다!

아래 링크는 커스텀 렌더 파이프라인(이하 SRP) 튜토리얼의 첫 번째 편입니다. 바로 이번 포스트에서 옮길 글이죠. (옮기다 보니 너무 길어서 반으로 나누어 포스팅하게 되었습니다.)


이번 글의 주제입니다.

  • 렌더 파이프라인 애셋과 인스턴스 생성하기
  • 카메라의 뷰를 렌더링 하기
  • 컬링, 필터링, 정렬을 수행하기
  • 불투명, 투명, 유효하지 않은 패스 분리하기
  • 하나 이상의 카메라 다루기

커스텀 SRP 시리즈의 첫 글이고, 렌더 파이프라인의 필수적인 초기화 작업을 다룹니다. 객체 관리 연재와 절차적 그리드 튜토리얼을 읽었다고 가정합니다. 유니티 2019.2.6f1을 사용해서 작성했습니다.

(이 분이 작성한 또 다른 SRP 연재가 있는데, 2018의 실험적인 API를 쓴 것이라 2018에서만 동작한다고 하네요. 이번 연재는 유니티 2019 이상을 위한 것이고 더 최신의 접근 방식을 취하지만, 비슷한 주제를 다룰 것이기 때문에 기다리기 싫은 사람은 2018 연재를 읽는 것도 괜찮을 것이라고 합니다.)

커스텀 렌더 파이프라인으로 렌더링

새 렌더 파이프라인

예전에는 유니티가 렌더링을 위해서 몇 가지 내장된 방법만을 지원했지만, 유니티 2018에서 SRP를 소개했고 우리가 원하는 것은 뭐든지 할 수 있게 되었습니다. 컬링과 같은 핵심적인 단계는 여전히 유니티에 의존해야 합니다. 유니티 2018에서는 두 개의 실험적인 렌더 파이프라인(이하 RP)을 추가했고, 바로 Lightweight RP와 High Definition RP입니다. 유니티 2019에서는 Lightweight RP는 더 이상 ‘실험판’이 아니고, 유니티 2019.3에서 Universal RP로 새롭게 브랜딩을 했습니다.

Universal RP는 기존의 RP를 대체하는 기본 RP가 될 예정이고, 커스터마이즈 하기 쉬우면서 대부분의 경우에 어울리는 하나의 RP이지만, 이 연재에서는 해당 RP를 커스터마이즈하기 보다는 아예 새롭게 전체 RP를 만들어보려고 합니다.

이번 글에서는 포워드 렌더링을 사용해서 언릿 셰이프를 그리는 최소한의 RP를 만들어 봅니다. 이후의 글에서 광원, 그림자, 다른 렌더링 방식을 비롯한 고급 기능을 추가하면서 확장해 나갈 수 있습니다.

프로젝트 셋업

유니티 2019.2.6 이상의 버전에서 새 3D 프로젝트를 만듭니다. 자체적인 파이프라인을 만들 거라, RP 프로젝트 템플릿중에 고르지는 않습니다. 프로젝트가 열리면 패키지 매니저에 가서 필요 없는 모든 패키지를 제거합니다. 이 튜토리얼에서는 UI 그리는 실험을 위해서 Unity UI만 사용할 예정이니 남겨 두셔도 됩니다.

선형 색 공간에서만 작업할 계획인데, 유니티 2019.2는 여전히 감마 색공간을 기본값으로 사용합니다. Edit / Project Settings 다음에 Player를 통해 Player Settings로 가서, Other Settings 섹션의 Color Space를 Linear로 바꿉니다.

색 공간이 선형으로 설정되었음

기본 씬을 몇 개의 오브젝트로 채우고, 스탠다드, 언릿 불투명, 투명 재질등을 사용합니다. Unlit/Transparent 셰이더는 텍스쳐가 필요합니다. 여기에 그곳에 사용할 UV 구체 맵이 있습니다.

UI 구(Sphere) 알파 맵, 검은 배경

저는 테스트 씬에 몇 개의 큐브를 놓았고, 모두 불투명합니다. 빨간 것들은 Standard 셰이더를 쓰는 재질이지만, 녹색과 노란색은 Unlit/Color 셰이더를 쓰는 재질입니다. 파란 구(sphere)들은 Rendering Mode를 Transparent로 지정한 Standard 셰이더를 쓰고, 하얀 구들은 Unlit/Transparent 셰이더를 씁니다.

테스트 씬

파이프라인 애셋

현재 내장 RP를 쓰고 있으니, 커스텀 RP로 바꾸려면 애셋 타입을 하나 만들어야 합니다. Universal RP와 비슷한 폴더 구조로 해볼게요. Custom RP 애셋 폴더를 만들고 그 아래 Runtime 자식 폴더를 만듭니다. CustomRenderPipelineAsset 타입의 새 C# 스크립트를 만들어 넣습니다.

폴더 구조

애셋 타입은 UnityEngine.Rendering 네임스페이스의 RenderPipelineAsset을 확장해야 합니다.

using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipelineAsset : RenderPipelineAsset {}

RP 애셋은 유니티에게 렌더링을 책임질 파이프라인 객체의 인스턴스를 전달하기 위한 것입니다. 애셋 자체는 단순한 핸들이자 설정을 저장할 공간일 뿐입니다. 아직은 설정은 없고, 우리 파이프라인 객체 인스턴스를 전달해봅시다. CreatePipeline 추상 메소드를 오버라이드해서 RenderPipeline 인스턴스를 반환하면 됩니다. 아직 커스텀 RP타입을 정의하지 않았으니, 우선은 null을 반환합니다.

CreatePipeline 메소드는 protected 접근자로 정의되어 있고, 이는 해당 메소드를 정의한 클래스 (여기서는 RenderPipelineAsset)과 자식 클래스만 접근할 수 있다는 뜻입니다.

protected override RenderPipeline CreatePipeline () {
		return null;
	}

이제 이 타입의 애셋을 프로젝트에 추가할 수 있도록, CustomRenderPipelineAsset에 CreateAssetMenu 속성을 추가합니다.

[CreateAssetMenu]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }

이는 Asset / Create 메뉴에 항목을 추가합니다. 깔끔하게 Rendering 서브 메뉴 안으로 옮기기 위해서는menuName 프로퍼티에 Rendering/Custom Render Pipeline을 대입합니다. 속성 타입 바로 뒤에 괄호 안에 넣으면 됩니다.

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }

(Catlike Coding 홈페이지에는 소스 코드 한 라인의 일부만 강조하는 기능이 있지만, 워드프레스에서는 아무리 찾아도 없어서 그냥 라인 단위로 강조했습니다. 혹시나 아시는 분 있으면 알려주세요~)

새 메뉴 항목을 통해 애셋을 프로젝트에 추가한 다음 Graphics 프로젝트 설정의 Scriptable Render Pipeline Settings에 추가한 애셋을 선택해 넣습니다.

Custom RP가 선택되었음

내장 RP를 교체하면 그래픽 설정의 많은 옵션이 사라집니다. 정보 패널에 설명이 나옵니다. 그리고 제대로 된 렌더링 파이프라인 없이 내장 RP를 비활성화해버렸기 때문에 아무것도 그려지지 않습니다. 게임 윈도우, 씬 윈도우, 재질 프리뷰는 이제 동작하지 않습니다. Window / Analysis / Frame Debugger 메뉴로 프레임 디버거를 열고 활성화 시키면, 정말로 게임 윈도우에 아무것도 그려지지 않는 것을 확인할 수 있습니다.

렌더 파이프라인 인스턴스

CustomRenderPipeline 클래스를 만들어서 CustomRenderPipelineAsset과 같은 폴더에 넣습니다. 우리 애셋이 반환할 RP 인스터스 타입이므로 RenderPipeline을 확장해야 합니다.

using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline {}

실제로 동작하는 파이프라인을 만들기 위해서는 RenderPipeline에 protected로 정의된 Render 추상 메소드를 오버라이드 해야 합니다. ScriptableRenderContext와 Camera 배열, 두 개의 매개변수를 갖습니다. 일단 구현은 비워둡니다.

	protected override void Render (
		ScriptableRenderContext context, Camera[] cameras
	) {}

CustomRenderPipelineAsset.CreatePipeline이 CustomRenderPipeline의 인스턴스를 반환하게 합니다. 아무것도 그리지는 않지만, 이를 통해 유효하고 동작하는 파이프라인이 됩니다.

	protected override RenderPipeline CreatePipeline () {
		return new CustomRenderPipeline();
	}

렌더링

매 프레임마다 유니티는 RP 인스턴스의 Render를 호출합니다. 그때 컨텍스트 구조체를 넘겨주는데, 이를 사용해서 네이티브 엔진과 소통하며 렌더링을 수행할 수 있습니다. 씬에 여러 카메라가 있을 수 있으므로 카메라의 배열도 넘겨줍니다. 카메라가 제공된 순서대로 그리는 것은 RP의 책임입니다.

카메라 렌더러

각 카메라는 개별적으로 렌더링 됩니다. 그러니 CustomRenderPipeline에서 모든 카메라를 그리는 대신에 하나의 카메라를 그리는 일을 전담하는 새로운 클래스를 만들겠습니다. CameraRenderer라고 이름을 지어주고 컨텍스트와 카메라를 받는 public Render 메소드를 추가합니다.

using UnityEngine;
using UnityEngine.Rendering;

public class CameraRenderer {

	ScriptableRenderContext context;

	Camera camera;

	public void Render (ScriptableRenderContext context, Camera camera) {
		this.context = context;
		this.camera = camera;
	}
}

CustomRenderPipeline이 생성될 때 렌더러의 인스턴스를 만들고, 그 인스턴스를 사용해 루프 안에서 모든 카메라를 그립니다.

	CameraRenderer renderer = new CameraRenderer();

	protected override void Render (
		ScriptableRenderContext context, Camera[] cameras
	) {
		foreach (Camera camera in cameras) {
			renderer.Render(context, camera);
		}
	}

우리의 카메라 렌더러는 Universal RP가 스크립터블 렌더러들과 거의 동일합니다. 이 접근방식을 사용하면 미래에 카메라 별로 다른 렌더링 방식을 취하기가 쉬워집니다. 예를 들어 하나는 일인칭 뷰이고 하나는 3D 맵 오버레이라던가, 하나는 포워드, 하나는 디퍼드 렌더링이라던가 말이죠. 하지만 지금은 모든 카메라를 같은 방식으로 그리겠습니다.

스카이박스 그리기

Camera.Render는 카메라가 볼 수 있는 모든 지오메트리를 그리는 임무를 수행합니다. 해당 작업을 별도의 DrawVisibleGeometry 메소드 안으로 옮겨서 명확하게 합니다. 기본 스카이박스를 그리는 것으로 시작해보시죠. 컨텍스트의 DrawSkybox를 호출하면서 카메라를 인자로 넘기면 됩니다.

	public void Render (ScriptableRenderContext context, Camera camera) {
		this.context = context;
		this.camera = camera;

		DrawVisibleGeometry();
	}

	void DrawVisibleGeometry () {
		context.DrawSkybox(camera);
	}

이렇게 해도 아직 스카이박스가 나오지는 않습니다. 우리가 컨텍스트에 전달한 명령들이 버퍼링 되기 때문입니다. 컨텍스트의 Submit을 호출해서 쌓여있는 작업을 내보내야 합니다. DrawVisibleGeometry 다음에 호출되는 별도의 Submit 메소드를 만들어 그 안에서 호출합시다.

	public void Render (ScriptableRenderContext context, Camera camera) {
		this.context = context;
		this.camera = camera;

		DrawVisibleGeometry();
		Submit();
	}

	void Submit () {
		context.Submit();
	}

마침내 게임 윈도우와 씬 윈도우에 스카이박스가 나옵니다. 프레임 디버거를 활성화하면 스카이박스를 그리는 항목을 볼 수 있습니다. Camera.RenderSkybox로 나오고, 그 아래 하나의 Draw Mesh 항목이 있는데, 이게 실제 드로우 콜을 나타냅니다. 이는 게임 윈도우의 렌더링에 대한 것이고, 다른 윈도우에서 그리는 것은 프레임 디버거에서 볼 수 없습니다.

스카이 박스가 그려졌음

카메라의 방향과 상관없이 스카이박스가 그려지는 점에 유의하세요. DrawSkybox에 카메라를 넘기기는 하지만 스카이박스가 그려져야 하는지를 판단하기 위해서만 사용되고, 이는 카메라의 Clear Flags를 통해 제어됩니다.

스카이박스 (그리고 전체 씬)을 올바르게 그리기 위해서는 뷰-투영(View-Projection) 행렬을 설정해야 합니다. 이 변환 행렬은 카메라의 위치와 방향(=뷰 행렬)을 카메라의 원근 투영 혹은 직교 투영 (=투영 행렬)을 결합한 것입니다. 셰이더에서는 unity_MatrixVP로 알 수 있고, 지오메트리가 그려질 때 사용되는 셰이더 프로퍼티들 중에 하나입니다. 프레임 디버거에서 드로우 콜을 선택하면 Shader Properties 섹션에서 이 행렬의 값을 볼 수 있습니다.

현재는 unity_MatrixVP 행렬이 항상 일정합니다. SetupCameraProperties 메소드를 통해 카메라의 프로퍼티를 컨텍스트에 적용해야 합니다. 이 메소드는 행렬뿐 아니라 몇 가지 다른 프로퍼티들도 설정합니다. DrawVisibleGeometry 전에 호출되는 별도의 Setup 메소드를 만들어서 그 안에서 호출합니다.

	public void Render (ScriptableRenderContext context, Camera camera) {
		this.context = context;
		this.camera = camera;

		Setup();
		DrawVisibleGeometry();
		Submit();
	}

	void Setup () {
		context.SetupCameraProperties(camera);
	}
올바르게 정렬된 스카이박스

명령 버퍼

컨텍스트는 우리가 내보내기 전까지 실제 렌더링을 지연시킵니다. 그 전에, 설정을 하고 나중에 실행할 명령들을 추가합니다. 스카이박스를 그리는 것과 같이 일부 작업은 전용의 메소드를 통해서 전달할 수 있지만, 다른 명령들은 별도의 명령 버퍼를 통해서 간접적으로 전달되어야 합니다. 씬 안에 있는 다른 지오메트리를 그리기 위해서는 그와 같은 버퍼가 필요합니다.

버퍼를 얻으려면 새 CommandBuffer 객체 인스턴스를 생성해야 합니다. 하나만 있으면 되니, CameraRender에서 기본적으로 하나를 생성하고 필드에 저장합니다. 또 프레임 디버거에서 인식할 수 있도록 버퍼에 이름을 지어줍니다. Render Camera 정도면 괜찮을 것 같습니다.

	const string bufferName = "Render Camera";

	CommandBuffer buffer = new CommandBuffer {
		name = bufferName
	};

객체 초기화 문법은 어떻게 동작하나요?

생성자를 호출한 후에 별도의 문장으로 buffer.name = bufferName; 이라고 적어준 것 같지만, 새 객체를 생성할 때 생성자 호출 뒤쪽에 코드 블럭을 덧붙일 수 있습니다. 그러고 나서 객체 인스턴스를 명시적으로 참조하지 않고도 객체의 필드와 프로퍼티에 값을 넣을 수 있습니다. 이는 그 필드와 프로퍼티들이 초기화된 후에만 인스턴스를 사용할 수 있다는 점을 명시적으로 해줍니다. 그 외에도, 다양한 매개변수를 받는 여러 버전의 생성자들을 만들지 않아도 한 문장만으로 초기화가 가능하게 해줍니다.

잘 보시면 생성자 호출에 빈 매개변수 리스트를 생략했습니다. 이는 객체 초기화 문법을 사용할 때 허용됩니다.


명령 버퍼를 사용해서 프로파일러 샘플을 넣어주면, 프로파일러와 프레임 디버거에서 보입니다. 적절한 지점에서 BeginSample과 EndSample을 호출하면 되는데, 우리의 경우는 Setup과 Submit의 도입부입니다. 두 메소드는 같은 이름을 사용해서 호출해야 하는데, 우리는 버퍼의 이름을 사용하겠습니다.

	void Setup () {
		buffer.BeginSample(bufferName);
		context.SetupCameraProperties(camera);
	}

	void Submit () {
		buffer.EndSample(bufferName);
		context.Submit();
	}

버퍼를 실행하려면, 버퍼를 인수로 넣으면서 컨텍스트의 ExecuteCommandBuffer를 호출합니다. 이때 버퍼에서 명령들은 복사되지만 지워지지는 않습니다. 버퍼를 재사용하려면 실행 후에 직접 지워줘야 합니다. 실행과 지우기는 항상 같이 호출하므로 하나의 메소드로 묶어주면 편리합니다.

	void Setup () {
		buffer.BeginSample(bufferName);
		ExecuteBuffer();
		context.SetupCameraProperties(camera);
	}

	void Submit () {
		buffer.EndSample(bufferName);
		ExecuteBuffer();
		context.Submit();
	}

	void ExecuteBuffer () {
		context.ExecuteCommandBuffer(buffer);
		buffer.Clear();
	}

Camera.RenderSkyBox 샘플이 이제 Render Camera 안에 포함되어 보입니다.

Render Camera 샘플

렌더 타겟 지우기

우리가 그리는 것은 결국에는 카메라의 렌더 타겟으로 렌더링 됩니다. 렌더 타겟은 기본적으로 프레임 버퍼이지만 렌더 텍스쳐가 될 수도 있습니다. 이전에 그려진 것들이 여전히 렌더 타겟에 남아 있어서 이번에 렌더링하는 이미지를 간섭할 수 있습니다. 올바른 렌더링을 보장하기 위해서 렌더 타겟을 지워서 오래된 내용을 제거해야 합니다. 명령 버퍼의 ClearRenderTarget을 호출하면 됩니다. Setup 메소드 안에서 수행하겠습니다.

CommandBuffer.ClearRenderTarget은 최소 3개의 인수가 필요합니다. 첫 두 개는 깊이와 색상 데이터를 지울지 여부이며, 둘 다 true로 넣습니다. 세 번째 인수는 지울 때 쓸 색상이고, 여기서는 Color.clear를 사용합니다.

	void Setup () {
		buffer.BeginSample(bufferName);
		buffer.ClearRenderTarget(true, true, Color.clear);
		ExecuteBuffer();
		context.SetupCameraProperties(camera);
	}
지우기, 중첩된 샘플로 보임

이제 프레임 디버거가 지우기에 대해서 Draw GL 항목을 보여주지만, 또 한 단계의 중첩된 Render Camera 안에 보여줍니다. 이는 ClearRenderTarget이 지우는 작업을 명령 버퍼의 이름을 가진 샘플로 감싸기 때문에 발생합니다. 우리의 샘플을 시작하기 전에 지워주면 불필요한 중첩을 없앨 수 있습니다. 이렇게 하면 두 개의 인접한 Render Camera 샘플 영역이 생기면서 병합됩니다.

	void Setup () {
		buffer.ClearRenderTarget(true, true, Color.clear);
		buffer.BeginSample(bufferName);
		//buffer.ClearRenderTarget(true, true, Color.clear);
		ExecuteBuffer();
		context.SetupCameraProperties(camera);
	}
지우기, 중첩 없음

Draw GL 항목은 Hidden/InternalClear 셰이더로 전체 화면 크기의 사각형을 그리는 것을 나타내는데, 이는 가장 효율적인 지우기 방법은 아닙니다. 우리가 카메라 프로퍼티를 설정하기 전에 지우기 때문에 이 방법이 사용됩니다. 두 단계의 순서를 바꿔주면 더 빠른 지우기 방법이 사용됩니다.

	void Setup () {
		context.SetupCameraProperties(camera);
		buffer.ClearRenderTarget(true, true, Color.clear);
		buffer.BeginSample(bufferName);
		ExecuteBuffer();
		//context.SetupCameraProperties(camera);
	}
올바른 지우기 방법

이제는 Clear (color+Z+stencil)이 보입니다. 이는 색상과 깊이 버퍼 모두 지워지는 것을 나타냅니다. Z는 깊이 버퍼를 의미하고 스텐실 데이타는 같은 버퍼의 일부입니다.

컬링

우리는 현재 스카이 박스를 볼 수 있지만, 씬에 넣어둔 어떤 객체도 볼 수 없습니다. 모든 객체를 그리는 대신에, 카메라에 비치는 객체만 그릴 겁니다. 씬 안에 렌더러 컴포넌트를 가진 모든 객체로 시작해서 카메라의 절두체 밖에 있는 것들을 컬링하는 방식을 사용합니다.

무엇이 컬링 될지 알기 위해서는 여러 카메라 설정과 행렬을 추적해야 하는데, 이를 위해 ScriptableCullingParameters 구조체를 사용할 수 있습니다. 우리가 직접 채우는 대신에, 카메라의 TryGetCullingParameters를 호출할 수 있습니다. 변질한 카메라 설정으로 실패할 수 있어서, 이 메소드는 파라메터들을 성공적으로 얻을 수 있는지를 반환합니다. 매개변수 값을 얻어내기 위해서는 앞에 out을 적어서 출력 인수로 제공해야 합니다. 이 작업을 성공 혹은 실패를 반환하는 별도의 Cull 메소드에서 수행하겠습니다.

	bool Cull () {
		ScriptableCullingParameters p
		if (camera.TryGetCullingParameters(out p)) {
			return true;
		}
		return false;
	}

왜 out을 적어야 하나요?

구조체 매개변수를 출력 매개변수로 정의하면 객체 참조와 같이 동작합니다. 이 참조는 인수가 존재하는 메모리 스택 상의 위치를 가리킵니다. 메소드 안에서 매개변수의 값을 바꾸면 복사본이 아닌 실제 값에 영향을 줍니다.

우리는 out 키워드를 보고 해당 메소드가 매개변수의 기존 값을 대체하여 올바르게 값을 넣어줄 책임이 있다는 것을 알 수 있습니다.

Try-get 메소드는 성공이나 실패를 반환하는 동시에 결과도 만들어내는 일반적인 방법입니다.


출력 인수로 사용할 때는 인수 리스트 안에서 바로 변수 선언을 할 수 있으니, 그렇게 해보겠습니다.

	bool Cull () {
		//ScriptableCullingParameters p
		if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
			return true;
		}
		return false;
	}

Render 안에서 Setup 전에 Cull을 호출하고 실패하면 중단합니다.

	public void Render (ScriptableRenderContext context, Camera camera) {
		this.context = context;
		this.camera = camera;

		if (!Cull()) {
			return;
		}

		Setup();
		DrawVisibleGeometry();
		Submit();
	}

실제적인 컬링은 컨텍스트의 Cull을 호출할 때 수행되고, 이 메소드는 CullingResults 구조체를 만듭니다. 성공했을 경우 Cull 안에서 이 작업을 수행하고 결과를 필드에 보관합니다. 이 경우에는 앞에 ref를 적어서 컬링 파라미터를 참조 인수로 넘겨주어야 합니다.

	CullingResults cullingResults;

	…
	
	bool Cull () {
		if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
			cullingResults = context.Cull(ref p);
			return true;
		}
		return false;
	}

왜 ref를 사용해야 하나요?

ref 키워드는 out과 같이 동작하지만, 해당 메소드가 무언가 값을 넣어줘야 할 의무는 없습니다. 메소드를 호출하는 쪽에서 처음에 값을 적절하게 초기화할 책임이 있습니다. 그래서 입력 혹은 입출력용으로 사용할 수 있습니다.

이 경우에 ref는 최적화를 위해 사용되었으며, 제법 크기가 큰 ScriptableCullingParameters의 복사본을 넘기는 것을 막기 위해서입니다. 메모리 할당을 방지하기 위해서 객체 대신에 구조체가 되는 것이 또 다른 최적화입니다.


지오메트리 그리기

이제 무엇이 보이는지를 알 수 있으므로 렌더링할 차례입니다. 컬링 결과를 인수로 넘기면서 컨텍스트의 DrawRenderers를 호출해서 어떤 렌더러를 사용할지 알려주면 됩니다. 그 외에도, 그리기 설정과 필터링 설정을 제공해야 합니다. 두 가지 모두 구조체로 (DrawingSettings와 FilteringSettings) 초기에는 기본 생성자를 사용하겠습니다. 두 개 모두 참조로 전달해야 합니다. DrawVisibleGeometry() 안에서 스카이박스를 그리기 전에 이 작업을 하세요.

	void DrawVisibleGeometry () {
		var drawingSettings = new DrawingSettings();
		var filteringSettings = new FilteringSettings();

		context.DrawRenderers(
			cullingResults, ref drawingSettings, ref filteringSettings
		);

		context.DrawSkybox(camera);
	}

아직 아무것도 보이지 않는데, 어떤 종류의 셰이더 패스를 허용할지 알려줘야 하기 때문입니다. 이번 튜토리얼에서는 언릿 셰이더만 지원하기 때문에 SRPDefaultUnlit 패스의 셰이더 태그 ID를 얻어와야 합니다. 이 ID는 한 번만 얻어서 정적 필드에 캐싱할 수 있습니다.

	static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");

이 태그 ID를 새 SortingSettings 구조체 값과 함께 DrawingSettings 생성자의 인자로 넣습니다. 카메라를 정렬 설정의 생성자에 넣으면, 직교(Orthographic) 혹은 거리 기반 정렬이 적용될지 결정하는 데 사용됩니다.

	void DrawVisibleGeometry () {
		var sortingSettings = new SortingSettings(camera);
		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		);
		…
	}

이 밖에도 어떤 렌더 큐들을 허용할지도 알려줘야 합니다. 모든 큐를 포함할 수 있게 FilteringSettings 생성자에 RenderQueueRange.all을 전달합니다.

		var filteringSettings = new FilteringSettings(RenderQueueRange.all);
언릿 지오메트리를 그리고 있음

언릿 셰이더를 쓰는 눈에 보이는 객체들만 그려지고 있습니다. 프레임 디버거의 RenderLoop.Draw 아래 모든 드로우 콜이 모여 있습니다. 투명한 객체가 좀 이상하지만, 우선 객체가 어떤 순서로 그려지는지 살펴보겠습니다. 프레임 디버거에서 드로우 콜을 차례로 선택하거나 화살표 키를 사용해서 단계를 확인할 수 있습니다.

프레임 디버거에서 그려지는 순서 따라가기

그리는 순서가 제멋대로입니다. 정렬 설정의 criteria 프로퍼티를 설정해서 특정한 그리기 순서를 강요할 수 있습니다. SortingCriteria.CommonOpaque를 사용합시다.

		var sortingSettings = new SortingSettings(camera) {
			criteria = SortingCriteria.CommonOpaque
		};
일반적인 불투명 정렬 (Common opaque sorting)

객체들이 이제 대략 앞에서 뒤로 그려지는데, 이는 불투명한 객체를 위해 이상적인 순서입니다. 어느 객체가 다른 객체의 뒤쪽에 그려지게 된다면, 가려진 프래그먼트들은 그리기를 건너뛸 수 있어서 렌더링 속도가 빨라집니다. 이 일반적인 불투명 정렬 옵션은 렌더 큐나 재질과 같은 몇 가지 다른 기준도 사용합니다.

불투명과 투명 지오메트리를 따로 그리기

프레임 디버거를 보면 투명 객체들이 그려지고 있지만, 불투명 객체 앞에 있지 않은 부분은 스카이박스에 덮어써 집니다. 스카이박스는 불투명 객체 다음에 그려지기 때문에 가려진 프래그먼트는 건너뛸 수 있지만, 투명한 지오메트리는 덮어써 집니다. 이는 투명 셰이더가 깊이 버퍼에 값을 쓰지 않기 때문에 발생합니다. 투명 객체 뒤쪽에 있는 것은 보여야 하므로, 투명 객체는 뒤에 있는 것을 가리지 않습니다. (가리지 않기 위해서 깊이 버퍼에 쓰지 않는다는 얘기입니다.) 해결책은 우선 불투명한 객체들을 그린 다음에 스카이박스를 그리고, 그다음에 불투명 객체들을 그리는 것입니다.

RenderQueueRange.opaque로 바꿈으로써 초기의 DrawRenderers 호출에서 투명한 객체들을 제거할 수 있습니다.

		var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

그다음 스카이박스를 그린 후에 DrawRenderers를 다시 호출합니다. 하지만 호출 전에 큐 범위를 RenderQueueRange.transparent로 바꾸세요. 정렬 기준도 SortingCriteria.CommonTransparent로 바꾸고 그리기 설정에 정렬 설정에 다시 대입합니다. 이렇게 하면 투명한 객체들이 그리기 순서가 뒤집힙니다.

		context.DrawSkybox(camera);

		sortingSettings.criteria = SortingCriteria.CommonTransparent;
		drawingSettings.sortingSettings = sortingSettings;
		filteringSettings.renderQueueRange = RenderQueueRange.transparent;

		context.DrawRenderers(
			cullingResults, ref drawingSettings, ref filteringSettings
		);
불투명, 다음에 스카이박스, 다음에 투명

왜 그리기 순서가 뒤집히나요?

투명한 객체들은 깊에 버퍼에 쓰지 않아서 앞에서 뒤로 정렬하는 것이 성능상 이점이 없습니다. 하지만 투명한 객체들이 시각적으로 서로의 뒤쪽에 위치하면 정확하게 블렌딩하기 위해서 뒤에서 앞으로 그려져야 합니다.

불행히도 뒤에서 앞으로 정렬하는 것이 정확한 블렌딩을 보장하지는 않습니다. 정렬은 객체 단위로 수행되며 객체의 위치 기반으로만 수행되기 때문입니다. 서로 교차하는 큰 투명 객체들은 여전히 부정확한 결과를 낳습니다. 이때 지오메트리를 더 작은 조각으로 잘라서 해결할 수 있는 예도 있습니다.


와우.. 여기가 원문의 딱 절반입니다. 글 한 편이 이렇게 긴 줄 몰랐네요. 너무 길어서 절반씩 나눠 포스팅해야 할 것 같습니다.

옮기는 것도 이렇게 오래 걸리는데 튜토리얼을 고안하고 코드를 작성하고 글까지 쓴 Jasper님의 노력은 정말 대단한 것 같습니다. 아래 버튼을 눌러서 Jasper님의 정기적인 후원자가 되거나 일회성 기부를 할 수도 있습니다. 튜토리얼이 마음에 드셨다면 Jasper님이 계속해서 튜토리얼을 쓸 수 있도록 도와주세요!

저를 응원하고 싶으시다면 트위터페이스북에서 구독/좋아요를 눌러주세요!

다음 글은 아래 링크에서 보실 수 있습니다.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay connected

58FansLike
56FollowersFollow
156FollowersFollow
128FollowersFollow

Recipe of the day