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

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

Catlike Coding의 Custom SRP 시리즈를 우리말로 옮기는 두 번째 글입니다. 지난번 글은 아래 링크에서 보실 수 있습니다.

에디터 렌더링

우리가 만든 RP가 언릿 객체들을 올바르게 그리기는 하지만, 유니티 에디터에서 작업 경험을 높이기 위해서 할 수 있는 일들이 있습니다.

레거시 셰이더 그리기

우리 파이프라인은 언릿 셰이더 패스만 지원하므로 다른 패스를 사용하는 객체는 렌더링 되지 않아 보이지 않습니다. 이것이 정확하기는 하지만 씬에 있는 일부 객체가 잘못된 셰이더를 사용한다는 사실을 숨깁니다. 어쨌든 그런 객체도 개별적으로 렌더링 되게 해봅시다.

누군가 기본 유니티 프로젝트로 시작한 후 나중에 우리의 RP로 전환하면 씬에 잘못된 셰이더를 가진 객체가 있을 수 있습니다. 모든 유니티 기본 셰이더를 다루려면 Always, ForwardBase, PrepassBase, Vertex, VertexLMRGBM, VertexLM 패스에 대한 셰이더 태그 ID를 사용해야합니다. 이 ID들을 정적 배열에 보관해두겠습니다.

	static ShaderTagId[] legacyShaderTagIds = {
		new ShaderTagId("Always"),
		new ShaderTagId("ForwardBase"),
		new ShaderTagId("PrepassBase"),
		new ShaderTagId("Vertex"),
		new ShaderTagId("VertexLMRGBM"),
		new ShaderTagId("VertexLM")
	};

보이는 지오메트리를 그린 후에 별도의 메소드에서 지원되지 않는 모든 셰이더를 그립니다. 우선 첫 번째 패스만 그려봅니다. 유효하지 않은 패스라서 어쨌든 결과는 잘못된 것이므로 다른 설정들은 신경 쓰지 않습니다. FilteringSettings.defaultValue 프로퍼티를 통해 기본 필터링 설정을 얻을 수 있습니다.      

	public void Render (ScriptableRenderContext context, Camera camera) {
		…

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

	…

	void DrawUnsupportedShaders () {
		var drawingSettings = new DrawingSettings(
			legacyShaderTagIds[0], new SortingSettings(camera)
		);
		var filteringSettings = FilteringSettings.defaultValue;
		context.DrawRenderers(
			cullingResults, ref drawingSettings, ref filteringSettings
		);
	}

그리기 순서 인덱스와 태그를 인수로 전달하면서 그리기 설정에서 SetShaderPassName 을 호출하여 여러 패스를 그릴 수 있습니다. 그리기 설정을 구성할 때 첫 번째 패스를 이미 설정했으므로 두 번째부터 시작하여 배열의 모든 패스에 대해 이 작업을 합니다.

		var drawingSettings = new DrawingSettings(
			legacyShaderTagIds[0], new SortingSettings(camera)
		);
		for (int i = 1; i < legacyShaderTagIds.Length; i++) {
			drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
		}
표준 셰이더가 검은 색으로 렌더링됩니다

표준 셰이더로 렌더링 된 객체가 보이기 시작했지만, 우리 RP가 필요한 셰이더 속성을 설정하지 않았기 때문에 지금은 검은 색으로만 표시됩니다.

에러 재질

어떤 객체가 지원되지 않는 셰이더를 사용하는지 명확하게 나타내기 위해 유니티의 에러 셰이더를 사용해 객체를 그리겠습니다. Hidden/InternalErrorShader 문자열을 인수로 Shader.Find를 호출해서 얻은 셰이더로 새 재질을 만듭니다. 정적 필드에 재질을  캐싱하여 매 프레임마다 새로운 머티리얼을 만드는 것을 방지합니다.  그런 다음 그리기 설정의 overrideMaterial 프로퍼티에 이 재질을 지정합니다.

	static Material errorMaterial;

	…

	void DrawUnsupportedShaders () {
		if (errorMaterial == null) {
			errorMaterial =
				new Material(Shader.Find("Hidden/InternalErrorShader"));
		}
		var drawingSettings = new DrawingSettings(
			legacyShaderTagIds[0], new SortingSettings(camera)
		) {
			overrideMaterial = errorMaterial
		};
		…
	}
마젠타 에러 셰이더로 렌더링됩니다.

이제 유효하지 않은 객체가 모두 보이고 잘못된 것을 확실하게 알 수 있게 되었습니다.

부분 클래스

유효하지 않은 객체를 그리는 것은 개발에 유용하지만 출시된 앱을 위한 것은 아닙니다. CameraRenderer의 에디터 전용 코드를 별도의 부분 클래스 파일로 옮깁시다. 원본 CameraRenderer 스크립트 애셋을 복사해서 이름을 CameraRenderer.Editor로 바꾸는 것으로 시작합니다.

하나의 클래스, 두 개의 스크립트 애셋.

그런 다음 원래 CameraRenderer를 부분 클래스 바꾸고 태그의 배열, 에러 재질,  DrawUnsupportedShaders 메소드를 제거합니다.

public partial class CameraRenderer { … }

부분 클래스란 무엇인가요?

하나의 클래스나 구조체 정의를 여러 부분으로 나누어 다른 파일에 보관하는 방법입니다. 단순히 코드를 정리하려는 목적으로 사용됩니다. 자동으로 생성된 코드와 직접 작성한 코드를 분리하는 것이 전형적인 사용 예입니다. 컴파일러 입장에서는 모두 같은 클래스 정의의 일부분입니다. 객체 관리, 더 복잡한 레벨 튜토리얼(영문)에서 소개되었습니다.

다른 부분 클래스 파일을 정리해서 앞에서 제거했던 것들만 남깁니다.

using UnityEngine;
using UnityEngine.Rendering;

partial class CameraRenderer {

	static ShaderTagId[] legacyShaderTagIds = {	… };

	static Material errorMaterial;

	void DrawUnsupportedShaders () { … }
}

에디터 관련 부분은 유니티 에디터에서만 필요하므로 UNITY_EDITOR  심볼로 감싸서 에디터에서만 동작하게 합니다.

partial class CameraRenderer {

#if UNITY_EDITOR

	static ShaderTagId[] legacyShaderTagIds = { … }
	};

	static Material errorMaterial;

	void DrawUnsupportedShaders () { … }

#endif
}

그러나 이 시점에서는 빌드가 실패하는데, 다른 부분 클래스 있는 DrawUnsupportedShaders를 호출하고 있고 이 메소드는 에디터에서만 존재하기 때문입니다. 이를 해결하기 위해 메소드도 부분 메소드로 만듭니다. 추상 메소드 선언과 유사하게 항상 메소드 시그니처앞에 partial을 선언하면 됩니다. 클래스 정의의 어느 부분에서나 할 수 있으므로 에디터 부분에 넣어 봅시다. 전체 메소드 선언에도 partial 표시가 있어야합니다.

	partial void DrawUnsupportedShaders ();

#if UNITY_EDITOR

	…

	partial void DrawUnsupportedShaders () { … }

#endif


빌드 컴파일이 성공했습니다. 컴파일러는 완전히 선언되지 않은 모든 부분 메소드의 호출을 제거합니다.

개발 빌드에 유효하지 않은 객체를 표시 할 수 있을까요?

네, 대신에 UNITY_EDITOR || DEVELOPMENT_BUILD 조건부 컴파일을 기반으로 할 수 있습니다. 그러면 DrawUnsupportedShaders가 개발 빌드에도 존재하고 여전히 릴리즈 빌드에는 포함되지 않습니다. 하지만 이 연재에서는 일관되게 개발과 관련된 모든 것을 에디터에만 제한시키겠습니다.

기즈모 그리기

현재 우리 RP는 씬 윈도우나 게임 윈도우에 기즈모가 활성화되어도 기즈모를 그리지 않습니다. 

기즈모가 없는 씬.

UnityEditor.Handles.ShouldRenderGizmos 를 호출하여 기즈모를 그릴 지 여부를 확인할 수 있습니다. 그려야 한다면 카메라를 첫 번째 인수로, 어떤 기즈모 서브셋을 그려야 하는지를 두 번째 인수로 하여 컨텍스트에 DrawGizmos 를 호출해야합니다. 이미지 효과 전과 후의 두 가지 서브셋이 있습니다. 이 시점에서 우리는 이미지 효과를 지원하지 않으므로 둘 다 호출하겠습니다. 새로운 에디터 전용 DrawGizmos 메서드에서 수행합니다.

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

partial class CameraRenderer {
	
	partial void DrawGizmos ();

	partial void DrawUnsupportedShaders ();

#if UNITY_EDITOR

	…

	partial void DrawGizmos () {
		if (Handles.ShouldRenderGizmos()) {
			context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
			context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
		}
	}

	partial void DrawUnsupportedShaders () { … }

#endif
}

기즈모는 다른 모든 것들이 그려진 뒤에 뒤에 그려야합니다.

	public void Render (ScriptableRenderContext context, Camera camera) {
		…

		Setup();
		DrawVisibleGeometry();
		DrawUnsupportedShaders();
		DrawGizmos();
		Submit();
	}
기즈모가 있는 씬.

Unity UI 그리기

유니티의 인게임 사용자 인터페이스에 주의를 돌려보겠습니다. 예를 들어 , GameObject / UI / Button 통해 버튼을 추가하여 간단한 UI를 만듭니다. 게임 윈도우에는 표시되지만 씬 윈도우에는 표시되지 않습니다.

게임 윈도우에서의 UI 버튼.

왜 UI 버튼을 만들 수 없나요?

프로젝트에 Unity UI 패키지가 포함되어 있어야 합니다.

프레임 디버거는 UI가 우리의 RP에 의해서가 아니라,  별도로 렌더링되었음을 보여줍니다.

프레임 디버거에서의 UI.

적어도 이는  캔버스 컴포넌트의의 Render Mode가 기본값인 Screen Space – Overlay 설정된 경우입니다. Screen Space – Camera 로 변경하고 기본 카메라를 Render Camera 로 사용하면 투명한 지오메트리의 일부가 됩니다.

프레임 디버거의 스크린 스페이스 카메라 UI.

UI는 씬 윈도우에서 렌더링 될 때 항상 World Space 모드를 사용합니다.  일반적으로 UI가 매우 크게 보이는 이유입니다. 그러나 씬 윈도우를 통해 UI를 편집 할 수는 있지만 그려지지는 않습니다.

씬 윈도우에 UI가 보이지 않습니다.

씬 윈도우에서 렌더링할 때는 UI를 명시적으로 월드 지오메트리에 추가해야 합니다. 카메라를 인수로 전달하면서 ScriptableRenderContext.EmitWorldGeometryForSceneView를 호출하면 됩니다. 이 작업을 새로운 편집기 전용의 PrepareForSceneWindow 메소드에서 수행하세요. cameraType 프로퍼티가 CameraType.SceneView와 같을 때 씬 카메라로 렌더링합니다.

	partial void PrepareForSceneWindow ();

#if UNITY_EDITOR

	…

	partial void PrepareForSceneWindow () {
		if (camera.cameraType == CameraType.SceneView) {
			ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
		}
	}

이때 씬에 지오메트리를 추가할 수 있으므로 컬링 전에 수행해야 합니다.

		PrepareForSceneWindow();
		if (!Cull()) {
			return;
		}
씬 윈도우에 UI가 표시됩니다.

여러 대의 카메라

씬에 하나 이상의 활성 카메라가 있을 수 있습니다. 그렇다면, 우리는 카메라들이 함께 작동할 수 있게 해야 합니다.

두 대의 카메라

각 카메라는 Depth 값을 가지며, 기본 메인 카메라의 경우 -1입니다. 깊이의 오름차순으로 렌더링 됩니다. 이것을 보려면 Main Camera를 복제해 이름을 Secondary Camera 바꾸고 Depth 를 0으로 설정하세요. MainCamera 태그는 하나의 카메라에서만 사용되는 것을 전제로 하기 때문에, 다른 태그를 지정하는 것이 좋습니다.

두 카메라 모두 하나의 샘플 범위로 그루핑되었습니다.

씬이 이제 두 번 렌더링 됩니다. 렌더 타겟이 중간에 지워지므로 결과 이미지는 여전히 동일합니다. 프레임 디버거에서 이를 확인할 수 있지만,  같은 이름을 가진 인접한 샘플 범위가 합쳐지기 때문에 하나의 Render Camera 범위가 됩니다.

각 카메라가 자체 범위를 갖는 것이 더 명확합니다. 이를 가능하게 하려면 에디터 전용의 PrepareBuffer 메소드를 추가해서 버퍼의 이름과 카메라의 이름을 같게 합니다. 

	partial void PrepareBuffer ();

#if UNITY_EDITOR

	…
	
	partial void PrepareBuffer () {
		buffer.name = camera.name;
	}

#endif

씬 윈도우를 준비하기 전에 호출하세요.

		PrepareBuffer();
		PrepareForSceneWindow();
카메라 별 별도의 샘플.

버퍼 이름의 변경을 다루기

이제 프레임 디버거에서 카메라마다 별도의 샘플 계층을 볼 수 있지만, 플레이 모드에 들어가면 유니티의 콘솔에서 BeginSample 과 EndSample 카운트가 일치해야한다는 경고 메시지로 가득차게 됩니다. 샘플과 버퍼에 다른 이름을 사용하기 때문에 혼동되는 겁니다. 게다가 카메라의 name 프로퍼티를 접근할 때마다 메모리가 할당되는데,  빌드에서 이렇게 되면 안됩니다.

두 가지 문제를 모두 해결하기 위해 SampleName 문자열 프로퍼티를 추가하겠습니다. 에디터인 경우에는 PrepareBuffer에서 버퍼의 이름을 따라서 설정하고, 아닌 경우 단순히 Render Camera 문자열에 대한 상수 별칭입니다.

#if UNITY_EDITOR

	…

	string SampleName { get; set; }
	
	…
	
	partial void PrepareBuffer () {
		buffer.name = SampleName = camera.name;
	}

#else

	const string SampleName = bufferName;

#endif

Setup 및 Submit 에서 샘플에 SampleName 을 사용하세요. 

	void Setup () {
		context.SetupCameraProperties(camera);
		buffer.ClearRenderTarget(true, true, Color.clear);
		buffer.BeginSample(SampleName);
		ExecuteBuffer();
	}

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

차이를 확인하기 위해 Window / Analysis / Profiler 를 통해 프로파일러를 열고 에디터에서 우선 실행합니다.Hierarchy 모드로 전환하고 GC Alloc 열을 기준으로 정렬합니다. GC.Alloc 을 두 번 호출해서 총 100 바이트를 할당하는 항목을 볼 수 있으며, 이는 카메라 이름을 얻어오면서 발생한 것입니다. 그 아래에 Main Camera와 Secondary Camera 이름이 샘플로 표시되는 것을 볼 수 있습니다.

별도의 샘플과 100B 할당을 보여주는 프로파일러.

다음으로 Development Build와 Autoconnect Profiler를 활성화 해서 빌드를 만듭니다.  빌드를 실행하고 프로파일러가 연결되어 기록하고 있는지 확인하세요. 이 경우 100 바이트의 할당은 발생하지 않고 하나의 Render Camera만 보입니다.

빌드를 프로파일링중.

다른 48 바이트는 무엇이 할당되는 건가요?

우리가 통제할 수 없는 카메라 배열입니다. 그 크기는 얼마나 많은 카메라를 렌더링하는지에 따라 다릅니다.


카메라 이름을 얻어오는 부분을 Editor Only 라는 프로파일러 샘플로 감싸서 빌드는 아니고 에디터에서만 메모리를 할당한다는 점을 명시적으로 만들 수 있습니다. 이 경우 UnityEngine.Profiling 네임 스페이스에서 Profiler.BeginSample 및 Profiler.EndSample 을 호출하면 됩니다. BeginSample에만 이름을 전달하면됩니다.

using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Rendering;

partial class CameraRenderer {

	…
	
#if UNITY_EDITOR

	…

	partial void PrepareBuffer () {
		Profiler.BeginSample("Editor Only");
		buffer.name = SampleName = camera.name;
		Profiler.EndSample();
	}

#else

	string SampleName => bufferName;

#endif
}
에디터 전용 할당이 명백해졌음.

레이어

카메라는 특정 레이어의 객체만 보이게 구성할 수도 있습니다. Culling Mask를 조정하면 됩니다. 동작을 확인하기 위해서 표준 셰이더를 사용하는 모든 객체를 Ignore Raycast 레이어로 옮겨봅시다.

레이어가 Ignore Raycast 로 전환되었습니다.

Main Camera 의 컬링 마스크에서 해당 레이어를 제외합니다.

Ignore Raycast를 컬링함

Secondary Camera가 볼 수 있는 유일한 레이어로 만듭니다.

Ignore Raycast 레이어 외의 모든 것을 컬링

Secondary Camera가 마지막에 렌더링하므로 유효하지 않은 객체만 보입니다.

게임 윈도우에 Ignore Raycast 레이어만 보임

클리어 플래그

렌더링 되는 두 번째 카메라의 클리어 플래그를 조정하여 두 카메라의 결과를 합칠 수 있습니다. 클리어 플래그는 카메라의 clearFlags 프로퍼티를 통해 가져올 수 있는 CameraClearFlags 열거형으로 정의됩니다. 지우기 전에 Setup 에서 값을 지정합니다.

	void Setup () {
		context.SetupCameraProperties(camera);
		CameraClearFlags flags = camera.clearFlags;
		buffer.ClearRenderTarget(true, true, Color.clear);
		buffer.BeginSample(SampleName);
		ExecuteBuffer();
	}

CameraClearFlags 열거형은 네 가지 값을 정의합니다. 1에서 4까지 Skybox , Color , Depth, Nothing 입니다. 이들이 실제로 독립된 플래그 값은 아니지만 조금씩 덜 지우는 것을 나타냅니다. 마지막 플래그를 제외한 모든 경우에 깊이 버퍼가 지워져야 하므로, Depth 보다 작거나 같은 경우에 깊이 버퍼를 지웁니다

		buffer.ClearRenderTarget(
			flags <= CameraClearFlags.Depth, true, Color.clear
		);

Skybox의 경우 어쨌든 이전의 모든 색상 데이터를 바꿔버리기 때문에, 플래그가 Color로 설정된 경우에만 색상 버퍼를 지우면됩니다. 

		buffer.ClearRenderTarget(
			flags <= CameraClearFlags.Depth,
			flags == CameraClearFlags.Color,
			Color.clear
		);

단색으로 지우는 경우에는 카메라의 배경색을 사용해야 합니다. 그러나 선형 색공간에서 렌더링하기 때문에 해당 색상을 선형 공간으로 변환해야 하고, 결국 camera.backgroundColor.linear가 필요합니다. 다른 모든 경우에는 색상이 중요하지 않으므로 Color.clear로 충분합니다. 

		buffer.ClearRenderTarget(
			flags <= CameraClearFlags.Depth,
			flags == CameraClearFlags.Color,
			flags == CameraClearFlags.Color ?
				camera.backgroundColor.linear : Color.clear
		);

Main Camera가 가장 먼저 렌더링 되므로, 메인 카메라의 Clear Flags 는 Skybox 또는 Color 로 설정해야합니다. 프레임 디버거가 활성화되면 항상 깨끗한 버퍼로 시작하지만, 이것이 일반적으로 보장되는 것은 아닙니다.

Secondary Camera의 클리어 플래그는 두 카메라의 렌더링이 합쳐지는 방식을 결정합니다. Skybox 또는 Color의 경우 이전 결과가 완전히 대체됩니다. Depth의 경우 Secondary Camera가 스카이 박스를 그리지 않는 것을 제외하고는 정상적으로 렌더링되므로, 이전 결과가 배경으로 표시됩니다. Nothing의 경우는 깊이 버퍼가 유지되므로, 언릿 객체는 마치 같은 카메라로 그린 것처럼 엉뚱한 객체를 가려버립니다. 그러나, 이전 카메라에서 그린 투명 객체에는 깊이 정보가 없으므로, 스카이 박스가 이전에 그랬던 것 처럼, 덮어서 그려집니다.

색상 버퍼 지우기, 깊이 버퍼 지우기, 아무것도 지우지 않기

카메라의 Viewport Rect를 조정하여 렌더링 영역을 전체 렌더 타겟의 일부로만 줄일 수도 있습니다. 나머지 렌더 타겟은 영향을 받지 않습니다. 이 경우 Clear / InternalClear 셰이더를 사용하여 지우기가 수행됩니다 . 렌더링을 뷰포트 영역으로 제한하는 데 스텐실 버퍼가 사용됩니다.

보조 카메라의 뷰포트를 줄이고, 색상 버퍼 지우기

프레임당 두 개 이상의 카메라를 렌더링한다는 것은 컬링, 설정, 정렬 등과 같은 일도 여러 번 수행해야 한다는 것을 의미합니다. 동일한 시점에서는 하나의 카메라를 사용하는 것이 일반적으로 가장 효율적인 방법입니다.

다음 튜토리얼은 Draw Calls 입니다.


여기까지가 원문의 첫 번째 글입니다. 재밌게 읽고 계신지 모르겠네요. 마음에 드셨다면 Jasper님이 계속해서 튜토리얼을 쓸 수 있도록 아래 버튼을 눌러 정기적인 후원자가 되거나 일회성 기부를 할 수 있습니다!

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

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay connected

58FansLike
56FollowersFollow
156FollowersFollow
128FollowersFollow
- Advertisment -

Recipe of the day