본문 바로가기
툰렌더링 공부

언리얼 엔진 뜯어보기 #3

by IX. 2023/04/05 11:24:02

4.5

회사일이 바빠서 한동안 엔진을 들여다보질 못했다.

최근에 알아본 것은 안티앨리어싱에 대해서인데

MSAA방식은, 적어도 포워드 셰이딩을 사용할 땐, PC에서 Masked머티리얼이 반투명으로 출력되는 버그를 일으킨다.

모바일에선 정상으로 나오는 걸로 보아 알파 투 커버리지 관련 코드가 말썽을 일으키는 게 아닐까 추정된다.

PC게임 만들면서 포워드 + MSAA를 쓸 이유가 그닥없기 때문에 방치하고 있는 걸지도 모르겠다.

 

이제 다시 엔진뜯기

MRT[0]이 파이널컬러일 줄 알았는데 의외로 그렇지 않았다. 

MRT[2]의 알파값이 1이면 BaseColor핀에 꽂힌 컬러대로 간접광을 일으킨다.

이에 따라 MRT의 합성과정에 대해 살펴볼 필요가 있다.

챗GPT에게 물어보자

...별로 도움이 안된다.

각 MRT는 PixelShaderOutputCommon.ush에서 OutTarget이란 이름으로 재정의되고

#if PIXELSHADEROUTPUT_MRT0
	OutTarget0 = PixelShaderOut.MRT[0];
#endif

#if PIXELSHADEROUTPUT_MRT1
	OutTarget1 = PixelShaderOut.MRT[1];
#endif

#if PIXELSHADEROUTPUT_MRT2
	OutTarget2 = PixelShaderOut.MRT[2];
#endif

#if PIXELSHADEROUTPUT_MRT3
	OutTarget3 = PixelShaderOut.MRT[3];
#endif

이걸 기반으로 코드를 뒤져보니 LumenCardPixelShader.usf에서 사용되고 있다.

OutTarget0 = float4(sqrt(DiffuseColor), Opacity);
OutTarget1 = float4(CardSpaceNormal.xy * 0.5f + 0.5f, 0.0f, /*bValid*/ 1.0f);
OutTarget2 = float4(Emissive, 0.0f);

일단 이걸 고장내 보자.

OutTarget0 = float4(1, 0, 0, 0);
OutTarget1 = float4(0, 1, 0, 0);
OutTarget2 = float4(0, 0, 1, 0);

큰일이다.. 너무 고장났다..

일단 복구해놓고... 

루멘의 아웃타겟들을 모두 0으로 만들면, 드디어 순수한 파이널 컬러를 얻을 수 있다.

하지만 여전히 뒤죽박죽이다...이에 대해선 내일 다시 알아보자.

4.6

언리얼 TA들이 간다는 컴파일 지옥.

 

Lit셰이더의 LightAccumlator을 0으로 하면 빛 계산 이전의 데이터를 돌려준다.

그런데 옆 모델은 전혀 다른 누적기를 사용하니, 제대로 된 빛계산이 나와야 할텐데..

그렇다면 빛을 0으로 한 채, MRT를 정상출력하면 언릿셰이더가 될까?

아니.. 잠깐 내가 뭘 한거지..

가정이 틀린 것 같다.루멘카드는 간접광을 조금 더 세게 때려주는 것 외의 큰 변화는 없다.

 

이리저리 만져보니 MRT[2].a는 반사값을 표현한다. 즉, 이건 색상이 아니라 컨텍스트란 이야기가 된다.

그래서 좀 더 파고 들어가니 DeferredShadingCommon.ush의 EncodeGBuffer()에 각 버퍼의 쓰임새가 정의되어 있다.

#if 1
		OutGBufferA.rgb = EncodeNormal( GBuffer.WorldNormal );
		OutGBufferA.a = GBuffer.PerObjectGBufferData;
#else
		float3 Normal = GBuffer.WorldNormal;
		uint   NormalFace = 0;
		EncodeNormal( Normal, NormalFace );

		OutGBufferA.rg = Normal.xy;
		OutGBufferA.b = 0;
		OutGBufferA.a = GBuffer.PerObjectGBufferData;
#endif

		OutGBufferB.r = GBuffer.Metallic;
		OutGBufferB.g = GBuffer.Specular;
		OutGBufferB.b = GBuffer.Roughness;
		OutGBufferB.a = EncodeShadingModelIdAndSelectiveOutputMask(GBuffer.ShadingModelID, GBuffer.SelectiveOutputMask);

		OutGBufferC.rgb = EncodeBaseColor( GBuffer.BaseColor );

이걸 통해 유추해본 G버퍼의 구조는...

A = 노말

B = PBR마스크 3총사

C = 순수 베이스컬러

 

아하... 그렇다면 B의 버퍼를 무력화시켜주면 완전한 회색을 만들 수 있지 않을까

Out.MRT[0] = half4(0.2f, 0.2f, 0.2f, 1);
/*Out.MRT[1] = half4(0, 0, 0, 1);	*/
Out.MRT[2] = half4(0, 0, 0, 1);	
Out.MRT[3] = half4(0, 0, 1, 1);

아.. 뭔가 완전하지 않아...

MRT[2].a를 0으로 바꿔주면

성공이다!

각 MRT가 GBuffer를 의미하는 것은 알아냈다.

이제 엔진이 이걸 어떻게 조합하는지 알아보자.

 

4.7

추가로 알아낸 사실이 있다. GBufferC.a는 AO,  GBufferD는 커스텀용이다.

#if ALLOW_STATIC_LIGHTING
		OutGBufferC.a = GBuffer.PrecomputedShadowFactors.x;
#else
		OutGBufferC.a = GBuffer.GBufferAO;
#endif
		OutGBufferD = GBuffer.CustomData;

디퍼드의 장점이 여러개의 라이팅 사용이기 때문에 Allow Static Lighting옵션은 반드시 켜져 있을 것이다. 그렇다면 AO대신 사전 계산된 팩터를 사용한다. 이게 무엇인지는 나중에 알아보기로 하고 일단 합성과정부터 알아보자.

 

MRT 2번은 G버퍼 B에 연결된다. 러프니스가 PBR머티리얼의 핵심요소이긴 하지만, 0이어도 베이스컬러는 나와야 한다. 그런데 실제로는 표현되지 않았다. 베이스 컬러에 의한 간접광만 표현됐다는 느낌이다.

MRT[2] (=GBufferB)의 알파채널은 셰이딩 모델에 따라 PBR의 계산을 달리 해주는 것 같다.

Out.MRT[2] = half4(1, 0, 0.5f, 1);
Out.MRT[2].a = EncodeShadingModelIdAndSelectiveOutputMask(SHADINGMODELID_DEFAULT_LIT, 0);

MainPS는 PixelShaderOutputCommon.ush에서 호출된다. 전에 보았듯 MRT 2번은 OutTarget2으로 아웃된다.

그런데 그 다음과정이 없다... OutTarget2가 사용되는 곳이 없는데...

 

해서 잘 보니 베이스패스에 다음과 같은 코드가 있다.

// all PIXELSHADEROUTPUT_ and "void FPixelShaderInOut_MainPS()" need to be setup before this include
// this include generates the wrapper code to call MainPS(inout FPixelShaderOutput PixelShaderOutput)
#if COMPUTE_SHADED
	#include "ComputeShaderOutputCommon.ush"
#else
	#include "PixelShaderOutputCommon.ush"
#endif

어..음.. 그럼 고장내 보자.

오히려 최종 아웃풋이 여기에 기록되고 있다.

PixelShaderOutputCommon.ush의 결과값은 최종적으로 뷰포트에 반영된다.

그렇다면 BasePassPixelShader.usf의 컬러가 최종 결과가 맞다는 결론이 나온다.

루멘의 간접광도 아마 라이트 어커멀레이터에서 기록될 것이다.

OutTarget이든, MRT든 무얼 변경해도 결과는 같다.

여전히 풀리지 않는 의문은 개별 요소를 합성하는 곳은 어디인가? 라는 것이다. 

다음에 알아보자

 

4.10

루멘카드에 지정된 OutTarget0은 실제로 간접광에 영향을 끼친다. 맨 왼쪽 캐릭터의 초록색은 코드에서 임의로 지정한 색깔이다. 그렇다면 간접광을 두 번 칠한다는 이야기인데, 왜일까. 

그런데 카드란 건 뭘까... 싶어서 찾다보니 이런 문서를 발견했다.

Lumen Siggraph 2022 (realtimerendering.com)

으으. 영어 싫다. 카드라는 건 직교뷰 카메라 뭉치(?)라고 한다. 카메라를 기준으로 헤이트필드를 만들고, 이를 기준으로 광선추적을 한다는 이야기 같다. 뭔소리인지 1도 모르겠다.^^

 

다시 돌아와서 실험. 루멘의 간접광은 노말을 기준으로 계산할까?

정답은 Yes. 폼셰이딩에 속한다. 

 

루멘카드를 다시 복구하고 이미시브를 화면에서 숨겨봤는데 오! 

화면에 보이지 않는 이미시브까지 계산해서 빛을 뿌려준다. SS(Screen Space)로는 부족하다고 생각한 걸까.

다시 꺼보니 확실히 알 수 있다.

그냥 간접광 한 번 더 덧칠해준다...정도로 이해하면 될 것 같다.

 

같은 자리만 맴돌고 있으니 더 이상의 탐색은 무의미하다.

실전으로 부딪혀보자.

도전과제 #1. 기본 툰렌더 만들기

우선 저 셰이딩의 근원을 알아야 하는데 못찾고 있음..

 

점입가경

4.11

헤더파일(.h)은 함수실행을 못하는 것으로 알고 있는데, .ush(언리얼 셰이더 헤더)는 상당히 많은 함수를 호출한다. 원래 되는 거였나...?

 

공식문서에는 FDeferredShadingSceneRenderer::Render로 코드분석을 시작하길 권하고 있다.

그래서 찾아봤더니 겁나 많다.

.cpp를 보는 순간부터 이미 내 능력 밖이지만, 계속 두드리면 열리지 않을까...

그래서 뒤에 (를 붙였더니 딱 하나 나온다.

찾았다!

함수 초반은 준비과정 같다. 옵션이 음청 많다 보니, 사전 준비도 많이 필요핟다.

아마 여기서부터 본격적으로 렌더를 시작하는 것 같다.

...그러다가 대뜸 베이스 패스 렌더?

이후는 AO, 스트라타 등등.

애초에 궁금한 것은 오파쿠 머티리얼의 렌더 순서였으니, 필요한 것들만 뽑아보면

1. BasePassRender()

2. RenderShadowDepthMaps()

3. RenderLumenSceneLighting()

4. RenderCustomDepthPass()

5. RenderDFAOAsIndirectShadowing()

6. RenderLights()

7. RenderDiffuseIndirectAndAmbientOcclusion()

8. RenderDeferredReflectionsAndSkyLighting()

 

이후는 헤어 머티리얼 혹은 볼륨포그, 햇살, 하늘...

....어휴. 복잡해. 하나도 모르겠다.

 

그림자는 RenderShadowDepthMaps에서 관리하는 게 아닐까 싶은데, 그렇다면 이걸 파이프라인에서 뺀다면 그림자가 그려지지 않는 것일까? 테스트해보자.

 

일단 빌드

큰일이다. 그냥 빼기만 했더니 고장났다...