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

언리얼 엔진 뜯어보기 #4

by IX. 2023/04/12 10:24:10

4.12

내가 수정하길 원하는 렌더링들은 대부분 베이스 패스 렌더에서 끝난다는 것은 알았다.

지금까지 가정한 것들을 정리해보면 루멘은 베이스 패스 렌더링 후 간접광을 한 번 더 덧그리는 식으로 구현된다.

그런데 베이스 패스렌더에서도 이미 간접광 계산은 되고 있다. 혹시 이것은 루멘이 아니라, 언리얼의 GI시스템이 기본으로 가지는 효과는 아닐까? 그렇다면 포워드 릿에서도 간접광이 계산이 될까?

...는 역시 안된다. 루멘이 맞다. 애초에 쓸만한 GI옵션의 선택지가 루멘뿐이다.

 

그럼 다시 베이스 패스의 렌더러로 돌아와보자.

라이팅 계산을 위한 BxDF의 색깔을 바꿔보자

아하-

해당 셰이더모델의 BxDF는 IntegrateBxDF()에서 호출된다. 그리고 이 함수는 AccumulateDynamicLighting()에서 호출되어 빛누적기에 정보를 더한다. 그리고 또 이 함수는 DeferredLightingCommon.ush에서 호출된다.

그리고 이 함수에... LightMask를 구하는 구문이 들어가 있다. 이것이 셀프셰도우가 아닐까?

주석처리시켜 고장내 보자.

if (LightData.bRadialLight)
	{
		//LightMask = GetLocalLightAttenuation( TranslatedWorldPosition, LightData, ToLight, L );		
		MaskedLightColor *= LightMask;
	}

결과는....

땡-!

중간에 하나 더 있다! 이번엔 과연...!

//float4 LightAttenuation = GetLightAttenuationFromShadow(InputParams, SceneDepth);
float4 LightAttenuation = 1;
float4 Radiance = GetDynamicLighting(DerivedParams.TranslatedWorldPosition, DerivedParams.CameraVector, ScreenSpaceData.GBuffer, ScreenSpaceData.AmbientOcclusion, ScreenSpaceData.GBuffer.ShadingModelID, LightData, LightAttenuation, Dither, uint2(InputParams.PixelPos), SurfaceShadow);

정답!

이 항목은 DeferredLightPixelShaders.usf의 DeferredLightPixelMain()에 정의되어 있다.

간접광은 계산 후 단순 더하기 같아 보인다. 로직이야 복잡하겠지만 결국 빛이니까...

 

라이트마스크는 찾았는데 이게 언제 폼셰이딩과 결합되는지 알아야 한다.

내일은 이걸 찾아보자

 

4.13

일단 폼셰이딩. 즉 NoL이  어디있는지 찾아보자. 의심되는 부분은 ShadingModels.ush의 BxDF함수이다.

으..하필이면 헤더파일. 광역 컴파일이 필요하다.

//Lighting.Diffuse *= AreaLight.FalloffColor * (Falloff * NoL);

빙고! 이 계산은 디퓨즈에 폼셰이딩을 더해준다.

예상대로라면 간접광은 이 디퓨즈를 기반으로 합성되어 MRT[3]에 저장될 것이다. 

그렇다면 바꿔보자.

//Lighting.Diffuse *= AreaLight.FalloffColor * (Falloff * NoL);
half ToonShade = smoothstep(0.5f, 0.51f, NoL);
Lighting.Diffuse *= ToonShade;

색과 셀프 쉐도우는 일부러 뺐으니 검은 그림자 + 간접광. 원하는 결과가 맞다.

간접광이 참 예쁘다.

여기에 셀프쉐도우를 복원해보자.

..어..음.. 예상과 좀 다르다.

주광 계산시 그림자에 디퓨즈 컬러를 반영하는 것일까?

...아니, 그냥 코드를 잘못건드린 실수였다.휴

 

OutColor에 LightAttenuation만 적용해보자.

...파란색? 왜 파란색이죠?!

내일 알아보도록 하자.

 

4.14

별달리 파란색일 이유는 없는데.. (오히려 빨강이 나오면 나왔지.)

해서 자세히 보니 이 코드가 원인인 것 같다.

float4 GetPerPixelLightAttenuation(float2 UV)
{
	return DecodeLightAttenuation(Texture2DSampleLevel(LightAttenuationTexture, LightAttenuationTextureSampler, UV, 0));
}

uv를 받아 색상으로 리턴하니 B채널이 항상 1이다. 포토샵에서 테스트를 해보니 파랑이 맞다.

어차피 R채널만 쓸테니 별 상관없어보인다. 호기심 해결!

이제 이렇게 뽑은 빛감쇠(감쇄와 감쇠 중 뭐가 맞지? 싶어 이것도 찾아보니 '감쇠'가 맞다.)는 그 다음 구문인 Radiance를 구하는 GetDynamicLighting()에서 사용된다.

float4 Radiance = GetDynamicLighting(DerivedParams.TranslatedWorldPosition, DerivedParams.CameraVector, ScreenSpaceData.GBuffer, ScreenSpaceData.AmbientOcclusion, ScreenSpaceData.GBuffer.ShadingModelID, LightData, LightAttenuation, Dither, uint2(InputParams.PixelPos), SurfaceShadow);

그리고 이 함수는 이를 다시 GetDynamicLightingSplit()로 토스해서

float4 GetDynamicLighting(
	float3 TranslatedWorldPosition, float3 CameraVector, FGBufferData GBuffer, float AmbientOcclusion, uint ShadingModelID, 
	FDeferredLightData LightData, float4 LightAttenuation, float Dither, uint2 SVPos, 
	inout float SurfaceShadow)
{
	FDeferredLightingSplit SplitLighting = GetDynamicLightingSplit(
		TranslatedWorldPosition, CameraVector, GBuffer, AmbientOcclusion, ShadingModelID, 
		LightData, LightAttenuation, Dither, SVPos, 
		SurfaceShadow);

	return SplitLighting.SpecularLighting + SplitLighting.DiffuseLighting;

다시 빛을 계산하는 AccumulateDynamicLighting()로 넘긴 후...(그만가!)

FDeferredLightingSplit GetDynamicLightingSplit(
	float3 TranslatedWorldPosition, float3 CameraVector, FGBufferData GBuffer, float AmbientOcclusion, uint ShadingModelID, 
	FDeferredLightData LightData, float4 LightAttenuation, float Dither, uint2 SVPos, 
	inout float SurfaceShadow)
{
	FLightAccumulator LightAccumulator = AccumulateDynamicLighting(TranslatedWorldPosition, CameraVector, GBuffer, AmbientOcclusion, ShadingModelID, LightData, LightAttenuation, Dither, SVPos, SurfaceShadow);
	return LightAccumulator_GetResultSplit(LightAccumulator);
}

이를 GetShadowTerms()으로 넘겨서

GetShadowTerms(GBuffer.Depth, GBuffer.PrecomputedShadowFactors, GBuffer.ShadingModelID, ContactShadowOpacity,
			LightData, TranslatedWorldPosition, L, LightAttenuation, Dither, Shadow);

마참내! Shadow.SurfaceShadow에 float형으로 저장한다.

float DynamicShadowFraction = DistanceFromCameraFade(SceneDepth, LightData);
			// For a directional light, fade between static shadowing and the whole scene dynamic shadowing based on distance + per object shadows
			Shadow.SurfaceShadow = lerp(LightAttenuation.x, StaticShadowing, DynamicShadowFraction);
			// Fade between SSS dynamic shadowing and static shadowing based on distance
			Shadow.TransmissionShadow = min(lerp(LightAttenuation.y, StaticShadowing, DynamicShadowFraction), LightAttenuation.w);

			Shadow.SurfaceShadow *= LightAttenuation.z;
			Shadow.TransmissionShadow *= LightAttenuation.z;

			// Need this min or backscattering will leak when in shadow which cast by non perobject shadow(Only for directional light)
			Shadow.TransmissionThickness = min(LightAttenuation.y, LightAttenuation.w);

이를 추적하다 깨달은 사실이 있는데, 그림자는 단색인데 4채널이나 되는 이유는 채널마다 각 정보를 다르게 담고 있기 때문이다. 코드를 통해 유추해보면

x : 표면그림자. 우리가 흔히 알고 있는 그 그림자. 

y : 투과그림자의 얇기

z : 포인트라이트의 그림자. 빛감쇠가 일어나는 방사형 그림자. bRadialLight라는 변수명으로 설정된다. 그렇구나! 어쩐지 변화가 없더라니...

w : 투과형 그림자의 최종형태...?

 

여기에 추가로 StaticShadowing을 합성. 이는 렌더링 설정의 Allow Static Lighting을 켜면 활성되는 코드로, 비활성화 시엔 그냥 1로 계산된다. 루멘은 static을 쓰지 않기 때문에 굳이 신경쓸 필요가 없을 것 같다.

 

그럼 이걸 이제 언제 폼셰이딩과 합치는가?

코드를 추적하다보니 LightAccumulator.ush에 LightAccumulator_AddSplit()라는 부분에서 최종적으로 합쳐진다.

기존의 폼셰이딩이 된 디퓨즈 이미지를 DiffuseTotalLight로 받고, 그림자는 CommonMultiplier로 받는다.

In.TotalLight += (DiffuseTotalLight + SpecularTotalLight) * CommonMultiplier;

이 공식을 찾기까지 참 오랜 시간이 걸렸다. 

코드가 맞는지 확인하기 위해 그림자의 색을 빨간색으로 바꿔보자.

너는 언제나 예측을 빗나가는구나아아아...

하지만 이로서 TotalLight는 언릿에서 사용한다는 걸 알았다. 그리고 이 경우엔 그림자 정보가 1로 넘어온다는 사실도.

 

 

음.. 아무래도 Split이니까..디퓨즈에만 섞어주는 편이 맞으려나...이번엔 초록색으로..!

In.TotalLightDiffuse += DiffuseTotalLight * (CommonMultiplier * float3(0, 1, 0));
In.TotalLightSpecular += SpecularTotalLight * CommonMultiplier;

아참..곱이 아니라 더해야지...

saturate를 했더니 너무 어두워졌다.

무엇보다 그림자의 색깔이 변하지 않았는데...

값을 올려서 다시 해보자. Green 10배 부스터!

그냥 전체적인 색감만 조율이 됐다. 또 엇나간 예측.

CommonMultiply가 그림자 정보는 맞나? 확인이 필요하다. 이를 1로 바꿔보자.

예상대로라면 그림자와 스펙큘라가 표시되지 않아야 한다.

땡!

그렇다면 먼저 조사하여야 할 것 LightAccumulator_AddSplit을 실행하지 않으면 그림자가 나오지 않을까?

주석처리하고 실행해 보니 그냥 형체가 표현되지 않았다. 그림자는 LightAccumulator에서 제어하는 것이 맞는 듯

이후는 다음시간에 알아보도록 하자.

 

4.17

LightAccumulator에 회색을 넣어준 결과. 하얗게 나온다.?

왜 하얀지는 모르겠다...

셀프 셰도우만 표현되는 걸로 보아 폼셰이딩이 여기서 합성되는 것은 맞다.

Surface Shadow를 흰색으로 맞추니, 완전 하얘졌다.

그럼 여기에 디퓨즈를 제 색으로 갖춰넣으면 포워드 셰이딩 같아질까.

간접광이 계산되기 때문에 완전히 같지는 않다. 흐음..

일단 폼셰이딩과 셀프쉐도우의 층분리를 해보자.

두개가 확고히 다른 컬러를 갖게 하고 싶다.

됐다! 성공이다.

LightAccumulator에 넘기는 정보는 디렉셔널 라이트의 컬러, 그리고 셰도우 마스킹뿐이다.

폼셰이딩을 붉게 했는데도 셀프쉐도우에 비치는 색이 그냥 텍스처 컬러를 따라가는 걸로 보아, 셀프쉐도우 내의 데이터는 G버퍼에 저장된 컬러정보를 참고하는 것 같다.

 

그럼 셀프쉐도우내의 간접광은 어디서 합성할까?

일단 LightAccumulator에 지정된 TotalLight가 간접광을 포함하는지에 대한 확인이 필요하다.

주광계산을 맡고 있는 함수 내에서 두 개의 Accumulator를 제외하면 이론적으로는 완전히 검은 색이 나와야 한다.

//LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, MaskedLightColor * Shadow.SurfaceShadow, bNeedsSeparateSubsurfaceLightAccumulation );			
//LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, MaskedLightColor * Shadow.TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );

만약에 이 결과에 간접광이 표현된다면, GI와 관련된 항목은 이후에 계산된다는 이야기가 된다.

테스트결과 GI는 따로 계산되고 있다. 간접광은 이후의 계산으로 보인다

사실 검은 그림자를 원하기 때문에 더 이상은 할 게 없다.

앰비언트를 밝게 하기 위해선 스카이라이트를 설치해서 간접광 수치를 높이면 충분히 밝아진다.

 

하지만 그래도 조절해보고 싶다..셰도우 컬러를 바꿔보자.

float3 shadowColor = float3(0, 1, 0) * CommonMultiplier;
In.TotalLightDiffuse += DiffuseTotalLight * (shadowColor + CommonMultiplier);

생각보다 잘 안된다... 어찌계산되고 있는 거지?

내일 다시 알아보도록 하자.