본문 바로가기

Unreal Engine

Part 4 : 디퍼드 셰이딩 파이프라인(The Deferred Shading Pipeline)

- 원본 URL : https://medium.com/@lordned/unreal-engine-4-rendering-part-4-the-deferred-shading-pipeline-389fc0175789

Input 데이터를 바꾸기

다른 타입의 메시들은 각각의 기능을 수행하기 위해 서로 다른 데이터들이 필요하다, 즉 : GPU Skin이 된 버텍스들은 단순 스태틱 메쉬들보다 더 많은 데이터가 필요할 것이라는 뜻이다. 언리얼은 이 차이를 CPU 쪽의 FVertexFactory 에서 다루지만, GPU 쪽에서는 약간 더 복잡하다.

왜냐하면 모든 버텍스 팩토리들은, 적어도 베이스 패스 상에서는 같은 버텍스 셰이더를 공유하기 때문이다. 버텍스 팩토리들은 FVetexFactoryInput이라는 인풋 스트럭처를 사용한다.

언리얼이 같은 버텍스 셰이더를 사용하지만 각 버텍스 팩토리에 대해서는 다른 코드를 포함하기 때문에, 언리얼은 FVertexFactoryInput 구조체를 각각의 버텍스 펙토리 내에서 재정의한다. 이 구조체는 GpuSkinVertexFactory.ush, LandScapeVertexFactory.ush, LocalVertexFactory.ush 그리고 몇 가지 다른 ush 에서 고유의 모습으로 재정의된다. 당연히 이 파일들을 모두 include할 수는 없을 것이다. - 대신 BasePassVertexCommon.ushVertexFactory.ush 를 인클루드한다. 이것은 셰이더가 컴파일 되었을 때 올바른 버텍스 팩토리에 셋팅되어, 엔진이 어느 FVertexFactoryInput implementation을 사용할 지를 알수 있도록 해준다. 앞에서 간단하게 살펴본 바와 같이, 버텍스 팩토리를 선언하는 데 사용되는 매크로에 셰이더 파일을 제공해야 하는데, 이것이 그 이유이다.

 

> Examples

FVertexFatoryInput 구조체는 이런 식으로 정의되어 있음

FVertexFactoryIntermediates는 요런식으로...

GPUSkinVertexFactory는 요런식으로 FVertexFactoryInput을 작성

 

이렇게 FMaterialPixelParamteres 라는 구조체도 있음. 설명에 있듯이 이 구조체는 머티리얼 Input으로 사용됨.

각 버텍스 팩토리별 Interpolants를 받아서 FMaterialPixelParameters로 변환한 것임.

 

이제 우리가 업로드하는 버텍스 데이터의 타입과, 베이스 패스 버텍스 셰이더에 들어갈 데이터 input이 일치한다. 다음 문제는 각각의 서로 다른 버텍스 팩토리들은 각각 다른, 버텍스 셰이더와 픽셀 셰이더 사이에 보간된 데이터들이 필요할 것이라는 것이다. 여기서 BasePassVertexShader.usf는 또다른 제네릭 함수를 호출한다 - GetVertexFactoryIntermediates, VertexFactoryGetWorldPosition, GetMaterialVertexParameters 함수가 그것이다. Find로 코드에서 찾아보면 VertexFactory.ush가 자신들이 필요한 대로 이 함수들을 고유하게 정의해 둔 것을 볼 수 있다.

 

BasePassVertexShader.usf 의 일부분

→ 지금은 GetVertexFactoryIntermediates 부분은 존재하지않음... 대신

VertexFactoryGetWorldPosition, GetMaterialVertexParameters는 아래와 같이 나와있음..

Output Data를 바꾸기

이제 우리가 버텍스 셰이더로부터 픽셀 셰이더로 어떻게 데이터를 얻는지 살펴보자. 당연하지만 BasePassVertexShader.usf의 아웃풋은 또다른 제네릭한 이름의 구조체인 FBasePassVSOutput 이고, 이것의 implementation은 버텍스 팩토리에 따라 결정된다. 그러나 여기에는 한 가지 함정이 있다 - 만약 Tessellation을 Enabled 시켰을 경우, 버텍스 셰이더 사이에는 Hull Shader와 Domain Shader이라는 두 단계가 더 존재한다. 그리고 이 단계들은 그냥 VS, PS 두 단계일때와는 다른 데이터들을 필요로 한다.

언리얼이 쓰는 또다른 트릭을 살펴보자. #define 문을 통해 FBasePassVSOutput 의 의미를 바꾸고 간단힌 FBasePassVSToPS 구조체로 심플하게 정의되거나, 테셀레이션을 위해 FBasePassVSToDS 로 정의될 수 있다. 두 구조체는 거의 비슷한 내용이지만, Domain Shader 버전은 몇 개의 변수를 추가한다.

 

이제, 버텍스 팩토리별 보간은 어떻게 할까? 언리얼은 FVertexFactoryInterpolantsVSToPSFBasePassInterpolantsVSToPSFBasePassVSOutput의 멤버들로 정의해서 해결한다.

  • FVertexFactoryInterpolantsVSToPS는 각각의 ~VertexFactory.ush 파일들 안에 정의되어 있으므로, Hull / Domain Shader 를 중간에 추가하더라도, 각 단계에서 여전히 올바른 데이터들을 전달해주고 있다.

  • FBasePassInterpolantsVSToPS 는 재정의되지 않는다 - 왜냐하면 이 구조체 안에 저장된 것들은 특정 버텍스 팩토리의 특별한 어떤 것에 따라 좌우되는 것이 아닌 버텍스포그 값이나 AmbientLightingVector 같은 것들을 담고 있기 때문이다.

언리얼의 재정의(redefinition) 테크닉은 대부분의 BasePass 버텍스 셰이더의 차이점을 추상화(abstracts away) 해버리면서, 테셀레이션 유무나 특정 버텍스 팩토리에 관계없이 사용될 수 있는 common code를 만들어낸다.

 

Base Pass 버텍스 셰이더

이제 디퍼드 셰이딩 파이프라인에서 각각의 셰이더가 실제로 무엇을 하는지 살펴보자.

BasePassVertexShader는 비교적 심플했다. 버텍스 셰이더의 대부분은 단순하게 BasePassInterpolantsVertexFactoryInterpolants를 계산하고 적용하는 것이다. 그러나 이 값들이 계산되는 방식은 조금 복잡하다 - 특정한 프리프로세서 define 안의 특정한 interpolator들을 선언하고 매칭되는 해당 define에만 적용되기 위해서 선택되는 특별한 경우들이 존재하기 때문이다.

예를 들어, 버텍스 셰이더 하단 부분이 define문을 보면

#IF_WRITES_VELOCITY_TO_GBUFFER 이라고 되어 있는 부분이 있다. 여기에서는 버텍스 단위로 이전 프레임과 현재 프레임간의 차이를 계산해 속도(VELOCITY)를 계산한다. 일단 계산된 다음에는 BasePassInterpolants 내의 변수에 값을 저장하지만, 자세히 들여다보면 그 변수의 선언을 같은 define문인 #IF_WRITES_VELOCITY_TO_GBUFFER 로 감싸놓은 것을 알 수 있다.

 

이것은 GBuffer에 velocity를 쓰는(write) 셰이더 변종들만 그 값들을 계산한다는 의미이다- 이것은 각 단계 간 전달되는 데이터의 양을 줄여 주는 데 도움이 되고, bandwidth를 줄여 결과적으로 셰이더를 빠르게 한다.

Base Pass 픽셀 셰이더

여기서부터 일이 약간 복잡해지기 시작한다. 마음의 준비를 하고, preprocessor check 내부의 대부분의 것은 없다고 가정하고 살펴보자.

Material 그래프에서 HLSL로

언리얼 내부에서 Material Graph를 생성하면, 언리얼은 당신의 노드 네트워크를 HLSL로 변환한다. 이 코드는 컴파일러를 통해 HLSL 셰이더에 삽입된다. MaterialTemplate.ush를 살펴보면, FPixelMaterialInput 와 같이, 내용 없는 구조체들이 아주 많은 것을 볼 수 있다. 이 안에는 그냥 %s 가 있다. 언리얼은 이것을 String format으로 사용하고, 머티리얼 그래프 코드별로 대치한다.

 

MaterialTemplate.ush 의 일부

 

이 텍스트 대치는 구조체에 국한되지 않는다. MaterialTemplate.ush 또한 implementation이 없는 여러 개의 함수를 포함하고 있다. 예를 들어, half GetMaterialCustomData0, half3 GetMaterialBaseColor, hald GetMaterialNormal은 모두 당신의 머티리얼 그래프를 기반으로 그들 내용을 채운다. 이것은 당신이 픽셀 셰이더로부터 이들 함수들을 호출하고, 머티리얼 그래프에서 당신이 작성한 계산을 수행하여 해당 픽셀에 대한 결과값을 돌려줄 수 있게 한다.

코드를 보면 "Primitive" 라는 이름의 변수를 참조하는 것을 볼 수 있지만, 이 변수에 대한 선언이 없다. 이것은 몇몇 매크로를 통해 C++ 쪽에서 선언된다. 이 매크로들은 각각의 Primitive가 GPU상에 그려지기 전에 렌더러에 의해 셋팅된 구조체를 정의한다.

PrimitiveUniformShaderParameters.h 의 가장 위쪽 매크로들에서 지원하는 변수들의 전체 리스트를 확인할 수 있다. 기본적으로 LocalToWorld, WorldToLocal, ObjectWorldPositionAndRadius, LightingChannelMask 등을 포함하고 있다.

GBuffer를 만들기

디퍼드 셰이딩은 Geometry Buffer를 뜻하는 "G-Buffer" 개념을 사용한다 - 이것은 지오메트리의 월드 노멀이나 베이스 컬러, 러프니스 등의 각기 다른 정보들을 저장하는 일련의 렌더 타겟들이다. 언리얼은 최종 셰이딩을 결정짓는 라이팅을 계산하는 데에 이 버퍼들을 샘플링한다. 그러나 언리얼이 G-Buffer를 보기 전에, 이들을 만들고 채우기 위해 몇 가지 단계를 거친다.

G-Buffer 의 내용은 제각기 다르지만, 각 채널의 수와 그 용도는 당신의 프로젝트 셋팅에 따라 서로 뒤섞일 수 있다. 보통의 경우 A~E 5개 텍스처의 G-Buffer이다.

  • GBufferA.rgb = World Normal 이고, 알파채널은 PerObjectGBufferData로 채워진다.

  • GBufferB.rgba = Metalic, Specular, Roughness, ShadingModelID

  • GBufferC.rgba = BaseColor 이고, 알파채널은 GBufferAO 로 채워진다.

  • GBufferD : custom data 전용

  • GBufferE 는 precomputed shadow 요소로 채워진다.

BasePassPixelShader.ushFPixelShaderInOut_MainPS 함수가 픽셀 셰이더의 입력지점(entry point)으로 기능한다. 수많은 preprocessor define문들 때문에 함수아 아주 복잡해 보이지만, 대부분 boilerplate code 이다. 언리얼은 GBuffer 를 위해 필요한 데이터들을 계산할 때, 당신이 어떤 라이팅 모델과 기능(features)들을 enabled 했는지에 따라 몇 가지 방법을 사용한다. 이 boilerplate 코드를 건드리지 않았다면, 제일 첫 번째로 중요한 함수가 아래쪽에 있다 - 이 함수에서 셰이더는 BaseColor, Metalic, Specular, MaterialAO, 그리고 Roughness 를 위한 값들을 얻는다. 이 함수는 MaterialTemplate.ush 내에 정의된 함수를 호출하여 이를 수행하며, 그 구현은 당신의 머터리얼 그래프 내에 정의되어 있다.

 

셰이더는 머터리얼 그래프의 호출에 의한 결과값들을 캐싱해 둔다 : 이것은 머터리얼 그래프의 함수들을 여러 번 호출하지 않기 위함이다.

이제 몇 가지의 데이터 채널 중 일부를 샘플링했으니, 언리얼은 이 중 일부를 특정 세이더 모델을 위해 수정할 것이다. 예를 들어, Subsurface Scattering(Subsurface, Subsurface Profile, Preintegrated Skin. two sided foliage 혹은 cloth) 을 사용하는 셰이딩 모델을 사용할 경우, 언리얼은 GetMaterialSubsurfaceData 함수를 호출해 이를 기반으로 Subsurface 색상을 계산해 낼 것이다. 만약 라이팅 모델이 이들 중 하나가 아닐 경우(?) 기본값인 0을 사용한다. subsurface 색상은 이후의 계산들의 일부이지만, 그 값에 write하는 셰이딩 모델을 쓰지 않는 이상 값은 단순히 0일 것이다.

Subsurface 색상을 계산한 이후, 언리얼은 GBuffer의 결과값을 수정하기 위해 DBuffer Decal을 허용할 것이다(만약 당신이 프로젝트에서 이를 허용했다면), 몇 가지 계산을 한 뒤 언리얼은 DBufferData를 BaseColor, Metalic, Roughness, Normal 그리고 Subsurface 색상 채널에 적용한다.

DBufferDecal이 데이터를 수정하게 한 뒤, 언리얼은 당신이 작성한 머터리얼 그래프의 결과값을 사용해 Opacity를 계산하고, Volumetric Lightmap을 계산한다. 마지막으로 FGBufferData 구조체를 생성하고, 이 모든 데이터를, 한 개의 픽셀을 나타내는 각 FGBufferData 인스턴스 내에 패킹한다

GBuffer 셰이딩 모델을 셋팅하기

언리얼이 다음으로해야 할 일은 각 셰이딩 모델이 GBuffer를 수정하는 것이다. 이를 위해 언리얼은 ShadingModelMaterials.ush 내부의 SetGBufferForSHadingModel 이라는 함수를 사용한다. 이 함수는 Opacity, BaseColor, Metalic, Specular, Roughness 그리고 Subsurface 데이터들을 가지고 각각의 셰이딩 모델이 GBuffer에 마음대로 적용할 수 있도록 한다.

대부분의 셰이딩 모델은 이 데이터를 수정하지 않고 그대로 적용하지만, 일부 셰이딩 모델(특히 Subsurface 관련된 것들)은 커스텀 데이터 채널을 사용해 추가적인 데이터를 GBuffer에 인코딩한다. 또하나 중요한 것은 이 함수가 ShadingModelID를 GBuffer 에 기록한다는 것이다. 이것은 픽셀 단위로 저장된 인티저 값으로, 각 픽셀이 어느 셰이딩 모델을 사용할 지 디퍼드 패스가 나중에 찾아볼 수 있도록 한다.

GBuffer에 커스텀 데이터 채널을 사용하고 싶을 경우, BasePassCommon.ush 를 수정해야 한다 - 이 파일은 WRITES_CUSTOMDATA_TO_GBUFFER 라는 preprocessor define문을 포함하고 있다. 만약 당신의 셰이딩 모델이 여기에 추가되었는지 확인하지 않고 GBuffer 의 Custom Data 부분을 사용하려고 시도하면, 그 값은 그냥 버려질 것이며 나중에 아무 값도 얻지 못할 것이다..

 

// Only some shader models actually need custom data.
#define WRITES_CUSTOMDATA_TO_GBUFFER	(USES_GBUFFER && (MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || MATERIAL_SHADINGMODEL_CLEAR_COAT || MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || MATERIAL_SHADINGMODEL_HAIR || MATERIAL_SHADINGMODEL_CLOTH || MATERIAL_SHADINGMODEL_EYE))

모델링 하나에 여러 개의 셰이딩 모델(Hair, Skin, Eye) 이 적용된 모습.

데이터를 사용하기

FGbufferData 구조체에 각각의 라이팅 모델이 그들이 가진 데이터를 어떻게 기록할 지 선택하게끔 했으니, BasePassPixelShader 는 약간의 boilerplate 코드와 하우스키핑 코드 - 픽셀 단위 velocity 계산, subsurface 색상 변경, ForceFullyRough 를 위한 러프니스 오버라이딩 등을 수행한다.

이 bolierplate 코드 이후에 언리얼은 미리 계산된 주변광(precomputed indirect lighting) 과 skylight 데이터(GetPrecomputedIndirectLightingAndSkyLight) 를 얻고 GBuffer의 DiffuseColor 에 이를 더한다.

// 모바일과 pc에서 달라졌던 부분이 바로 이부분인가....?

translucent 포워드 셰이딩, 버텍스 포그, 디버깅과 관련된 코드도 꽤 존재한다. 그리고 마침내 FGBufferData 구조체의 끝에 다다랐을 때, 언리얼은 EncodeGBuffer (DeferredShadingCommon.ush) 함수를 호출하는데, 이 함수는 FGBufferData 구조체를 받아 A~E까지의 다양한 GBuffer 텍스처에 이를 기록한다.

이게 BasePassPixelShader의 대부분을 마무리한다. 라이팅과 섀도우에 관련된 언급은 이 함수에 없다는 것을 눈치챘을 것이다. 이것은 디퍼드 렌더러에서는 이 계산들이 나중으로 보류되어(deferred) 있기 때문이다! 이 부분은 나중에 살펴보자.

리뷰

BasePassPixelShader는 당신의 머터리얼 그래프를 기반으로 생성된 함수들을 호출해 다양한 PBR 데이터 채널들을 샘플링하는 것을 담당한다. 이 데이터는 하나의 FGBufferData 에 패킹되는데, 이 데이터는 다양한 셰이딩 모델을 기반으로 이 데이터들을 수정해주는 함수들에게 전달된다. 셰이딩 모델은 텍스처에 기록된 ShadingModelID를 결정해서 이를 기반으로 나중에 어떤 셰이딩 모델을 선택할 지 를 결정한다. 마지막으로 FGBufferData 속의 데이터가 여러 개의 렌더 타겟에 인코딩되고 이는 이후에 사용된다.

Deferred Lighting Pixel Shader

이제 DeferredLightPixelShader.usf 를 살펴볼 텐데. 여기에서 각각의 라이트가 픽셀 하나에 미치는 영향이 계산된다. 이것을 위해 언리얼은 간단한 버텍스 셰이더를 사용해서 각 라이트가 영향을 미칠 수 있는 범위, 즉 포인트 라이트에는 Sphere, Spot Light에는 Cone 과 같은 알맞은 지오메트리를 그린다. 이렇게 하면 픽셀 셰이더를 실행해야하는 픽셀에 마스크가 만들어 지므로 적은 면적의 픽셀을 채우는 조명의 비용이 적어진다(cheaper).

섀도우가 있는 라이트와 그렇지 않은 라이트

언리얼은 라이팅을 여러 단계에 걸쳐서 그린다. 그림자를 드리우지 않는 라이트들이 처음 그려지고, 그 다음으로 Light Propegation 볼륨을 통한 indirect lighting이 그려진다. 그리고 마지막으로 그림자를 드리우는 라이트가 그려진다. 이들이 다른 점은 그림자를 드리우는 라이트에 추가적으로 필요한 프리-프로세싱에 있다. 각각의 라이트에 언리얼은 ScreenShadowMaskTexture를 계산하는데, 이는 장면 내의 그림자가 드리워진 픽셀의 스크린 스페이스 표현이다.

 

Sphere가 있는 장면의 ScreenShadowMaskTexture

이를 위해서, 언리얼은 장면 내의 각각의 오브젝트의 바운딩 박스에 해당하는 지오메트리를 렌더링한다. 언리얼은 장면 내의 오브젝트들을 다시 렌더링하는 게 아니고 GBuffer를 샘플링해 해당 픽셀의 depth 와 비교해서 라이트의 그림자를 드리우는 길목에 있는지를 체크한다. 복잡하게 들리는가? 그렇다. 여기에서 우리가 알아갈 만한 것은 각각의 그림자를 드리우는 라이트가, 어떤 표면들이 그림자 안에 있는지에 대한 스크린스페이스 representation을 계산하고, 이 데이터가 이후에 사용된다는 점이다.

Base Pass Pixel Shader

이제 그림자를 드리우는 라이트가 스크린스페이스 그림자 텍스처를 생성한다는 것을 알았으니, Base Pass Pixel Shader 가 어떻게 동작하는지 다시 살펴보자. 앞에서 보았듯이, 이것은 장면의 각 라이트마다 실행되므로, 여러 개의 라이트 영향 아래에 있는 오브젝트들은 픽셀 당 여러 번 실행 될 것이다. 이 픽셀 셰이더는 꽤 심플하지만, 이 픽셀 셰이더가 호출하는 함수를 눈여겨보자.

 

함수가 두 가지밖에 없으므로 각각이 어떤 일을 하는지 살펴보자.

GetScreenSpaceData는 GBuffer으로부터 해당 픽셀의 정보를 얻는다. SetUpLightDataForStandardDeferred 는 라이트 방향, 라이트 색상, falloff 등등의 정보를 계산한다. 마지막으로, GetDynamicLighting 을 호출하고 우리가 여태까지 계산한 모든 데이터 - 픽셀이 어디에 있는지, GBuffer 데이터는 무엇인지, 어떤 셰이딩 모델 id를 사용할지, 그리고 빛이 관한 정보를 넘겨준다.

GetDynamicLighting

GetDynamicLighting(DeferredLightingCommon.ush 안에 있는) 함수는 복잡하고 길어 보이지만 대부분의 복잡한 부분들은 각 라이트에 대한 여러 가지 셋팅들 때문이다. 이 함수는 SurfaceShadow와 SubsurfaceShadow 변수들을 계산하는데, 모두 1.0으로 초기화된다. - 만약 그림자가 존재한다면, 값은 1보다 작아진다. 나중에 여기에 값을 곱할 것이므로 이 점은 중요하다 - 더 높은 값일수록 그림자가 적게 진다는 것을 기억하자.

섀도잉이 enabled 되면 GetShadowTerms 가 호출된다. 이 함수는 해당 픽셀의 shadow term을 결정하기 위해 앞서의 Light Attenuation 버퍼를 사용한다(ScreenShadowMaskTexture 이라고 블리었던). 그림자 데이터는 수많은 곳으로부터 올 수 있지만, (언리얼은 light function + 오브젝트당 그림자를 z채널에, 오브젝트별 sub surface scattering 을 w에, 전체 scene의 디렉셔널 라이트 셰도우를 x에, 그리고 모든 scene의 디렉셔널 라이트 sub surface scattering을 y에 저장한다) 그리고 적절한 GBuffer 채널로부터 오는 static shadowing과 GetShadowTerms 는 그들의 정보를 앞서 언급한 SurfaceShadow 와 SubsurfaceShadow 변수에 저장한다.

 

더보기
void GetShadowTerms(FGBufferData GBuffer, FDeferredLightData LightData, float3 WorldPosition, float3 L, float4 LightAttenuation, float Dither, inout FShadowTerms Shadow)
{
	float ContactShadowLength = 0.0f;
	const float ContactShadowLengthScreenScale = View.ClipToView[1][1] * GBuffer.Depth;

	BRANCH
	if (LightData.ShadowedBits)
	{
		// Remapping the light attenuation buffer (see ShadowRendering.cpp)

		// LightAttenuation: Light function + per-object shadows in z, per-object SSS shadowing in w, 
		// Whole scene directional light shadows in x, whole scene directional light SSS shadows in y
		// Get static shadowing from the appropriate GBuffer channel
		float UsesStaticShadowMap = dot(LightData.ShadowMapChannelMask, float4(1, 1, 1, 1));
		float StaticShadowing = lerp(1, dot(GBuffer.PrecomputedShadowFactors, LightData.ShadowMapChannelMask), UsesStaticShadowMap);

		if (LightData.bRadialLight)
		{
			// Remapping the light attenuation buffer (see ShadowRendering.cpp)

			Shadow.SurfaceShadow = LightAttenuation.z * StaticShadowing;
			// SSS uses a separate shadowing term that allows light to penetrate the surface
			//@todo - how to do static shadowing of SSS correctly?
			Shadow.TransmissionShadow = LightAttenuation.w * StaticShadowing;

			Shadow.TransmissionThickness = LightAttenuation.w;
		}
		else
		{
			// Remapping the light attenuation buffer (see ShadowRendering.cpp)
			// Also fix up the fade between dynamic and static shadows
			// to work with plane splits rather than spheres.

			float DynamicShadowFraction = DistanceFromCameraFade(GBuffer.Depth, LightData, WorldPosition, View.WorldCameraOrigin);
			// 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);
		}

		FLATTEN
		if (LightData.ShadowedBits > 1 && LightData.ContactShadowLength > 0)
		{
			ContactShadowLength = LightData.ContactShadowLength * (LightData.ContactShadowLengthInWS ? 1.0f : ContactShadowLengthScreenScale);
		}
	}

#if SUPPORT_CONTACT_SHADOWS
	if ((LightData.ShadowedBits < 2 && (GBuffer.ShadingModelID == SHADINGMODELID_HAIR))
		|| GBuffer.ShadingModelID == SHADINGMODELID_EYE)
	{
		ContactShadowLength = 0.2 * ContactShadowLengthScreenScale;
	}

	#if MATERIAL_CONTACT_SHADOWS
		ContactShadowLength = 0.2 * ContactShadowLengthScreenScale;
	#endif

	BRANCH
	if (ContactShadowLength > 0.0)
	{
		float StepOffset = Dither - 0.5;
		float ContactShadow = ShadowRayCast( WorldPosition + View.PreViewTranslation, L, ContactShadowLength, 8, StepOffset );
		
		Shadow.SurfaceShadow *= ContactShadow;

		FLATTEN
		if( GBuffer.ShadingModelID == SHADINGMODELID_HAIR ||  GBuffer.ShadingModelID == SHADINGMODELID_EYE  )
		{
			// If hair transmittance is enabled, sharp shadowing should already be handled by 
			// the dedicated deep shadow maps. Thus no need for contact shadow
			const bool bUseComplexTransmittance = (LightData.HairTransmittance.ScatteringComponent & HAIR_COMPONENT_MULTISCATTER) > 0;
			if (!bUseComplexTransmittance)
			{
				Shadow.TransmissionShadow *= ContactShadow;
			}
		}
		else
			Shadow.TransmissionShadow *= ContactShadow * 0.5 + 0.5;
	}
#endif

	Shadow.HairTransmittance = LightData.HairTransmittance;
	Shadow.HairTransmittance.OpaqueVisibility = Shadow.SurfaceShadow;
}

 

이제 Surface와 Subsurface 데이터에 대한 그림자 정보를 모두 결정했다. Attenuation은 빛으로부터의 거리에 기반한 에너지에 falloff 이고 다양한 효과를 위해 조절될 수 있다 : 예를들어, 툰 셰이딩에서는 흔히 Attenuation을 계산에서 제거해 광원으로부터의 거리가 상관 없게끔 한다. 언리얼은 SurfaceAttenuation와 SubsurfaceAttenuation을 거리, 빛의 radius 그리고 falloff 에 따라 각각 따로 계산한다. 섀도잉은 Attenuation과 결합된다 - 이것은 이후의 계산에 attenuation strength만 들어간다는 것을 의마한다.(?)

Attenuation(거리 + 그림자)는 Light Accumulator 가 실행되기 전까지는 계산에 넣어지지 않는다. Light Accumulator는 표면 라이팅과 atteunation을 계산에 넣고, surface와 subsurface 라이팅을 light attenuation 값으로 곱한 뒤 모두 더한다.

마지막으로 DynamicLighting 함수는 Light Accumulator 로 축적한 전체 라이트를 반환한다. 실제로 이것은 그저 surface + subsurface 라이팅일 뿐이지만 코드는 subsurface 프로퍼티들과 디버그 옵션때문에 아주 복잡해 보인다.

ComputeLightProfileMultiplier

마지막으로 DeferredLightPicelShader 가 하는 것은 GetDynamicLighting 에서 계산된 색상을 ComputeLightProfileMultiplier 에서 나온 값에 곱하는 것이다. 이 함수는 1D IES 프로파일 텍스처를 사용할 수 있게 한다(이게뭐지?).만약 IES Light Profile이 라이트에 사용되고 있지 않다면 결과값은 바뀌지 않는다.

축적된 라이트 (Accumulated Light)

BasePassPixelShader가 오브젝트에 영향을 미치는 모든 라이트에 대해 실행되므로, 언리얼은 이 라이팅을 축적해서 버퍼에 저장한다. 이 버퍼는 몇 단계 후인 ResolvedSceneColor 단계를 거치기 전까지는 스크린에 그려지지 않는다. 전통적인 포워드 렌더링 방식으로 그려지는 반투명 오브젝트, 스크린 스페이스 TempAA, SSR과 같은 몇 가지 추가적인 것들이 계산들이 그 전에 이루어진다.

리뷰

각각의 라이트마다 섀도우 데이터가 스크린 스페이스에서 걔산된 후, 스테틱 섀도우, 서브서페이스 섀도우와 디렉셔널 섀도우를 모두 병합한다. 그리고 각 라이트마다의 근사 지오메트리가 그려진 이후 각각의 픽셀들마다의 해당 라이트의 영향이 그려진다. 서피스 셰이딩은 GBuffer 데이터와 셰이딩 모델에 기반해서 계산되며 라이트 감쇠값이 곱해진다. 라이트 감쇠는 라이트 세팅(거리, falloff 등)과 섀도우 샘플링의 조합이다. 각 표면에 대한 결과각의 라이트마다 섀도우 데이터가 스크린 스페이스에서 계산된 후, 스테틱 섀도우, 서브서페이스 섀도우와 디렉셔널 섀도우를 모두 병합한다. 그리고 각 라이트마다의 근사 지오메트리가 그려진 이후 각각의 픽셀들마다의 해당 라이트의 영향이 그려진다. 서피스 셰이딩은 GBuffer 데이터와 셰이딩 모델에 기반해서 계산되며 라이트 감쇠값이 곱해진다. 라이트 감쇠는 라이트 세팅(거리, falloff 등)과 섀도우 샘플링의 조합이다. 각각의 표면 셰이딩에 대한 결과값은 최종 라이트 값을 산출하기 위해 모두 함께 축적된다.