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

언리얼 엔진 뜯어보기 #5

by IX. 2023/04/21 11:30:39

4.21

회사일이 바빠서 한동안 엔진을 건드리지 못했다.어디까지 했었지?

마지막 글을 보니 셀프쉐도우의 색을 바꾸는 시도를 하고 있었다. 기록은 중요하다.

 

LightAccumulator.ush에서 의심되는 부분을 바꿨었다. LightAccumulator_AddSplit()에서 그림자 정보를 CommonMultiplier라는 float3형으로 넘겨주는데, 이 정보에 색을 더해준다면 그림자 색을 밝게 보정할 수 있을 줄 알았지만, 실제로는 그렇지 않았다.

셀프쉐도우가 정말 CommonMultiplier뿐인가?를 확인할 필요가 있다. 

먼저 이를 흰색으로 바꿔서 그림자가 사라지는 지 살펴보자.

CommonMultiplier = 1;
In.TotalLightDiffuse += DiffuseTotalLight * CommonMultiplier;

으...컴파일...

한 줄 바꾸고 10여분을 기다려야 한다.

매트호프만의 블로그 글에서 본 셰이더 순열 줄이기가 아니었으면 더욱 오래걸렸을 것이다.

(이전에 Allow Static Light옵션 하나 잘못켰을 뿐인데 셰이더순열이 2천개 가량 늘어난 경험이 있다.)

주절거리는 사이 컴파일 끝. 예상은 빗나갔다.

CommonMultiplier에 그림자를 넘겨주기는 하지만 그게 그림자는 아니다.(?!)

그렇다면 Shadow.SurfaceShadow가 정확한 정보인지 확인해보자.

너마저! 아.. 혼란스럽다.

그럼 빛감쇠를 분리하자마자 Surface에 정보에 회색을 넣는다면 어찌될까.

함수내에선 FShadowTerms으로 선언된 Shadow구조체를 사용하고, SurfaceShadow는 InOut으로 설정되어 따로 내보내진다.

그렇다면 이 그림자 정보를 어디에선가 갖다 쓴다는 이야기가 된다.

아마도 간접광 계산시 쓰일 것 같지만, 마스킹 용도로만 사용한다면 그림자를 그리는데 큰 영향을 끼치지는 못할 것이다.

그렇다면 역시 중요한것은 빛감쇠에 사용되는 로컬 변수인 Shadow.SurfaceShadow

2가지 테스트가 필요하다. 로컬은 회색으로, 전역은 제대로 된 데이터로 내보내보자.

예상이 맞는데?! 나머지 테스트는 필요없게 됐다.

내가 뭘 놓치고 있는 걸까...SurfaceShadow는 함수를 넘길 때 인자로 MaskedLightColor를 곱한다. 코드를 거슬러 올라가면 이 변수는 단순히 라이트의 컬러를 받아온다. 딱히 역할은 없어 뵈지만, 이걸 건드려보기로 하자.

단순 컬러가 아니야?! 그럼 MaskedLightColor를 제외하고 넘긴다면, 그림자가 전혀 표현되지 않는걸까?

어쩐지 그렇네..

그럼 100배를 곱한다면 분리가 될까?

MaskedLightColor = 100;

왠걸...변화가 없네. 

아... 임시로 수정해놓은 코드를 수정하지 않아 발생하던 문제였다.ㅇㄹㄴㅁㅇㄴㄹ

다시 100배 도전 

 

컴파일 되는 동안 LightData구조체를 살펴보자.

LightColor는 LightData구조체에서 가져오는 정보인데, 이게 정보가 많다.

struct FDeferredLightData
{
	float3 TranslatedWorldPosition;
	half  InvRadius;
	/** Premultiplied with light brightness, can be huge. Do not use half precision here */
	float3 Color;
	half  FalloffExponent;
	float3 Direction;
	float3 Tangent;
	float SoftSourceRadius;
	half2 SpotAngles;
	float SourceRadius;
	float SourceLength;
	half SpecularScale;
	float ContactShadowLength;
	/** Intensity of non-shadow-casting contact shadows */
	float ContactShadowNonShadowCastingIntensity;
	float2 DistanceFadeMAD;
	half4 ShadowMapChannelMask;
	/** Whether ContactShadowLength is in World Space or in Screen Space. */
	bool ContactShadowLengthInWS;
	/** Whether to use inverse squared falloff. */
	bool bInverseSquared;
	/** Whether this is a light with radial attenuation, aka point or spot light. */
	bool bRadialLight;
	/** Whether this light needs spotlight attenuation. */
	bool bSpotLight;
	bool bRectLight;
	/** Whether the light should apply shadowing. */
	uint ShadowedBits;
	float RectLightBarnCosAngle;
	float RectLightBarnLength;
	/** Rect light atlas info. */
	float2 RectLightAtlasUVOffset;
	float2 RectLightAtlasUVScale;
	float  RectLightAtlasMaxLevel;
	FHairTransmittanceData HairTransmittance;
};

하지만 다 라이트 디테일 탭에서 볼 수 있는 것들이다. 특별한 건 없어 보인다.

라이트 컬러 100배! 생각보다 밝지는 않다.

다시 시도해보자. 그림자의 색을 바꾸려면 그림자 감쇠에 색을 더하면 된다.

(그런데 이거 전에 한 번 했던 것 같기도 한데...) 

소용없다. 간접광을 계산할 때 그림자의 색은 무조건 까만색으로 맞춰지는 것 같기도 하다. 혹은 AO가 계산되어 곱해지고 있는지도 모르겠다.

감쇠값을 줄이고 색을 더해보면 어떤 결과를 나타낼까?

Shadow.SurfaceShadow = Shadow.SurfaceShadow * 0.25f;						
LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, MaskedLightColor + Shadow.SurfaceShadow, bNeedsSeparateSubsurfaceLightAccumulation );

그림자는 여전히 진한 걸로 보아 SurfaceShadow는 다른 부분에서 덧그려지고 있는것처럼 보인다.

이는 루멘을 꺼도 마찬가지이다.

하지만 포인트라이트를 추가하는 순간 라이트 적용방식이 변한다

이건 뭘까... 내일 알아보도록 하자.

 

4.24

DeferredLightingCommon.ush의 LightAccumuator부분을 수정한 지 꽤 오랜 시간이 흘렀음에도 여전히 다이렉트 라이트의 그림자 제어를 알아내지 못하고 있다. 포인트라이트에는 반응하는 걸로 볼 때 1라이트라면 그림자를 무조건 검은색으로 출력하는 걸까?라는 의심도 든다. 그렇다면 그림자를 텍스처로 받자마자 밝게 해주면 어떤 결과를 보일까.

일단 의심은 의심으로 끝났다. 하지만 아무래도 밖으로 나가는 Out.SurfaceShadow가 신경쓰인다. 순서를 바꿔보면 어떨까

SurfaceShadow = Shadow.SurfaceShadow;
//임시
Shadow.SurfaceShadow = saturate(Shadow.SurfaceShadow + 0.5f);

순서는 아무래도 상관이 없었다. 결과적으로, LightAttenuation이 그림자를 결정하는 것은 맞다.

그렇다면 더더욱 이상한디..

 

셰도우 컬러를 조절하던 중 흥미로운 걸 발견했다. LightAccumulator에 넘기기 전, 셰도우 마스크를 초록색으로 강제 설정했더니, 경계선부분에 초록색이 묻어나는 걸 확인할 수 있었다.

간접광 계산을 위해 설정한 그림자영역이 다른 곳에서 계산되어 한 번 더 계산되어 덧그려지고 있는 것이 아닐까?

만약 이 가설이 맞다면, 서페이스 셰도우를 흰색으로 해줄 경우에도 그림자가 출력되어야 한다. 테스트해보자.

역시 그렇다. 지금까지의 모든 혼란은 그림자를 2번 그리는 데서 왔던 것이다. 

2번째 셀프쉐도우 덧그리기가 GI를 위한 것이라면, GI를 제거하면 어떨까.

이건 여전하다. 그냥 계산을 안할 뿐, 한 번 더 덧그리기는 수행하고 있다. 하긴.. 디퍼드 렌더링 파이프라인자체가 이미지 합성인데 중간에 그걸 빼는 파이프라인을 다시 한 번 구축하는 것이 더 어려운 일 같기도 하다.

 

그렇다면 일전에 했던 LightAttenuation의 실험. 일단 코드를 임의로 바꿔둔 상태에서의 베이스렌더에선 그림자를 흰색으로 출력하고 있다.

이후의 렌더에서 그림자 정보를 가져다 쓴다면, Shadow구조체에 있는 서페이스 셰도우를 변경하고 Out으로 내보내는 값은 그대로 두어보자.

가설이 맞다면 '그림자 두번째 그리기'가 둘 중 어느값을 쓰는지 확실해진다.

'두번째 그림자 그리기'는 Shadow구조체의 값을 참조한다. 그럼 Out.surface는 도무지 어디 쓰는지 알 수가 없지만... 또 하나 이상한건 그림자를 절반은 남겨뒀는데 아예 사라진 것도 이상하다. 빛이 너무 강한가? 싶어서 조절해봤지만 이도 아니다.  언리얼이 대비가 좀 쎄긴 하지만, 그래도 희미한 자국이라도 남아야 한다. .. 흐음. 그럼 이번엔 1/4만 남겨보자.어떤 결과를 보일까?

Shadow.SurfaceShadow = saturate(Shadow.SurfaceShadow + 0.25f);

여전하다. 그럼 그냥 주석처리를 하면 어떨까?

??? 그럼 saturate가 문젠가?

아니다.  그럼 아주 작은 값만을 더해준다면?

그래도 사라진다. 0과 1의 처리가 나뉜다...는 결론을 얻을 수 있다.

그렇다면 서페이스 셰도우의 값을 0이상으로 아주 작게 맞춘 후, 실제 빛계산에선 초록색으로 변주를 해준다면?

짠-짜자자잔!

그럼 첫 가설이 맞는 거였잖아...에잉...

그럼 이제 '2번째 그림자'를 그리는 부분을 찾아보자.

LightMask > 0으로 검색하면 몇가지가 나온다.

이 중에서 스트라타는 빼고, 하나하나 짚어보자. 가장 의심이 가는 건 루멘.

그런데 전부 아니다..

 

아.. 혹시 간접광이 아니라 포인트 라이트를 위한 그림자 패스가 따로 있는 게 아닐까?

내일 알아보도록 하자.

 

4.25

포인트라이트 부분부터 점검해보자.

...아니 근데 이게 뭐야

if (Shadow.SurfaceShadow + Shadow.TransmissionShadow > 0)

등잔밑이 어둡다더니, 그림자를 수정하는 함수 내에 이런 분기를 타고 있었다....

아이고 지금까지의 일이 헛짓이..

if (1)
//if( Shadow.SurfaceShadow + Shadow.TransmissionShadow > 0 )

다시 테스트를 해보자.

난 지금까지 뭘 했던 거야...흑흑

하지만 그것이 RnD니까.

 

이제 포인트 라이트가 추가되면 흰색으로 처리되는 문제에 대해 고민해볼 필요가 있다.

일단 기존의 디렉셔널 라이트를 흰색으로 돌린다.

하지만 여전히 포인트라이트를 추가했을 때 문제가 일어난다. Lit셰이더에서는 문제가 일어나지 않는 걸로 보아 새로 추가한 셰이더모델의 문제다. 뭘 잘못건드린 걸까

 

일단 NoL의 툰셰이딩을 빼보자.

그게 문제 맞다. 뭐가 문젤까... 애초에 NoL의 L이 포인트라이트를 가르키는 것이 맞나?

포인트라이트의 이동에 반응하는 걸 보니 맞긴 하다. 포인트 라이트의 경우 L은 조명의 위치를 반전시켜 반환하는 것 같다.

테스트를 위해 그림자 색을 파란색으로 설정해놓았는데 생각해보니 이건 불필요한 코드다.

프로젝트 설정에서 포인트 라이트의 그림자를 설정해놓지 않았기 때문이다.

이런 에러가 뜬다.

실제로도 안쓸 예정이라 이건 제껴두기로 하자.

 

기존에 툰셰이더의 색을 테스트하기 위해 0.5f를 더하고 있었는데 이게 2번 중첩되며 마냥 하얘지는 것 같다.

그렇다면 포인트 라이트를 적용할 때 색을 다르게 써보면 어떨까?

//Lighting.Diffuse *= AreaLight.FalloffColor * (Falloff * NoL);
half ToonShade = smoothstep(0.5f, 0.6f, NoL);
#if NON_DIRECTIONAL_DIRECT_LIGHTING
	Lighting.Diffuse *= saturate(AreaLight.FalloffColor * (Falloff * ToonShade) + float3(1, 0, 0));
#else	
	Lighting.Diffuse *= saturate(AreaLight.FalloffColor * (Falloff * ToonShade) + float3(0, 0, 1));
#endif

 

하나는 맞고 하나는 틀렸다.

포인트 라이트 여부를 가리기 위한 분기가 작동하지 않는다.

아마 저 헤더가 돌 때쯤엔 선언되지 않는 것 같다.

 

사실 큰 상관이 없다. 툰렌더에 굳이 포인트 라이트를 넣고 싶었던 것은 캐릭터 앞 등불이었기 때문이다.

 

다음은 NoL의 색을 셀프쉐도우와 합쳐보자.

그런데 곰곰히 생각해보면 이것이 쉽지 않아 보인다.

 

지금까지 알아본 바로는 NoL은 BxDF에서 계산되어 디퓨즈 컬러(텍스처)와 함께 Lighting정보에 합성된다.

그리고 이렇게 합성된 디퓨즈에 캐스팅 셰도우를 더한 후, 간접광을 추가로 그려준다.

 

이것이 내가 지금까지 짜던 셰이더코드와 차이가 있다.

이걸 공부하기 전까진 앰비언트의 색깔을 직접 바꿀 생각이었지만, 실제로 코드를 살펴본 결과 엔진은 NoL과 셀프쉐도우를 각기 다른 곳에서 처리하고 있었다. 

 

왼쪽이 Lit, 오른쪽이 개조된 TestLit

루멘을 사용하므로 앰비언트의 컬러는 스카이라이트로 실시간 제어할 수 있다. 

이것만으로도 어느 정도 제어가 가능해보이지만, NPR이 목표이므로 부위별의 명암톤을 제어할 수 있어야 한다.

예를 들어 캐릭터의 다리부분은 피부톤이므로 붉은 색 명암을 띄어야 자연스럽다.

 

이에 대해선 내일 알아보자.